diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 8cfec413..e0cbb5b1 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -19,9 +19,47 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'release' }} jobs: - # Run tests, examples, and build docs + # Run the test suite with and without the xorq extra. The no-xorq leg + # verifies xorq is truly optional (its tests skip, not error). test: - name: Test & Build + name: Test (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: full + sync: uv sync --all-extras + - name: no-xorq + sync: uv sync --all-extras --no-extra xorq --no-extra examples + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies (${{ matrix.name }}) + run: ${{ matrix.sync }} + + - name: Verify import works + run: uv run python -c "import boring_semantic_layer; print('import OK')" + + - name: Run tests + run: uv run pytest + + - name: Run no-xorq-compatible examples + if: matrix.name == 'no-xorq' + run: make examples-core + + # Build examples, docs, and skills (require the full env incl. Node). + build: + name: Build (examples + docs + skills) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,8 +85,14 @@ jobs: uv sync --all-extras cd docs/web && npm ci - - name: Run all checks (tests + examples + docs build) - run: make check + - name: Run examples + run: make examples + + - name: Build docs + run: make docs-build + + - name: Check skills are up to date + run: make skills-check - name: Upload docs artifact if: github.event_name == 'release' @@ -60,7 +104,7 @@ jobs: deploy-docs: name: Deploy Docs if: github.event_name == 'release' - needs: test + needs: [test, build] runs-on: ubuntu-latest environment: name: github-pages @@ -74,7 +118,7 @@ jobs: release-pypi: name: Release to PyPI if: github.event_name == 'release' - needs: test + needs: [test, build] runs-on: ubuntu-latest environment: name: release diff --git a/Makefile b/Makefile index 175df596..b486e7d1 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ help: @echo " make test IBIS_VERSION=all - Run tests with all ibis versions (9.5.0, 10.8.0, 11.0.0)" @echo " make test IBIS_VERSION=10.8.0 - Run tests with specific ibis version" @echo " make examples - Run all example scripts" + @echo " make examples-core - Run no-xorq-compatible examples" @echo " make examples IBIS_VERSION=all - Run examples with all ibis versions" @echo " make examples IBIS_VERSION=10.8.0 - Run examples with specific ibis version" @echo " make docs-build - Build documentation" @@ -105,6 +106,19 @@ else @echo "✓ All examples passed!" endif +examples-core: + @echo "Running no-xorq-compatible examples..." + @for file in \ + scripts/demo_bsl_v2.py \ + examples/quickstart.py \ + examples/window_functions.py \ + examples/bucketing_with_other.py \ + examples/sessionized_data.py; do \ + echo "Running $$file..."; \ + uv run --with . "$$file" || exit 1; \ + done + @echo "✓ No-xorq-compatible examples passed!" + # Build docs docs-build: @echo "Building documentation..." diff --git a/examples/bucketing_with_other.py b/examples/bucketing_with_other.py index 354dd796..86112807 100644 --- a/examples/bucketing_with_other.py +++ b/examples/bucketing_with_other.py @@ -6,9 +6,13 @@ from pathlib import Path -import xorq.api as xo +import ibis from ibis import _ +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import from_yaml @@ -28,13 +32,13 @@ def main(): nest={"data": lambda t: t.group_by(["code", "elevation"])}, ) .mutate( - rank=lambda t: xo.row_number().over( - xo.window(order_by=xo.desc(t.avg_elevation)), + rank=lambda t: xibis.row_number().over( + xibis.window(order_by=xibis.desc(t.avg_elevation)), ), ) .mutate( is_other=lambda t: t.rank > 4, - state_grouped=lambda t: xo.ifelse(t.rank > 4, "OTHER", t.state), + state_grouped=lambda t: xibis.ifelse(t.rank > 4, "OTHER", t.state), ) .group_by("state_grouped") .aggregate( diff --git a/examples/quickstart.py b/examples/quickstart.py index 356ca9d8..a5dfbb12 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -12,7 +12,10 @@ import ibis import pandas as pd -import xorq.api as xo + +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis from boring_semantic_layer import to_semantic_table @@ -112,7 +115,7 @@ def main(): .with_measures(sum_val=lambda t: t.value.sum()) ) - rolling_window = xo.window(order_by="date", rows=(1, 1)) + rolling_window = xibis.window(order_by="date", rows=(1, 1)) expr4 = ( ts_st.group_by("date") .aggregate("sum_val") diff --git a/examples/sessionized_data.py b/examples/sessionized_data.py index 266cb173..a38a52a9 100644 --- a/examples/sessionized_data.py +++ b/examples/sessionized_data.py @@ -12,7 +12,10 @@ import ibis import pandas as pd -import xorq.api as xo + +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis from boring_semantic_layer import from_yaml, to_untagged @@ -32,7 +35,7 @@ def main(): # Filter for carrier WN on 2002-03-03 and add flight_date column filtered_flights = flights.filter( - lambda t: (t.carrier == "WN") & (t.dep_time.date() == xo.date(2002, 3, 3)), + lambda t: (t.carrier == "WN") & (t.dep_time.date() == xibis.date(2002, 3, 3)), ).mutate(flight_date=lambda t: t.dep_time.date()) # Create sessions with nested flight legs @@ -53,7 +56,7 @@ def main(): ]), }, ) - .mutate(session_id=xo.row_number().over(xo.window())) + .mutate(session_id=xibis.row_number().over(xibis.window())) .order_by("session_id") ) diff --git a/examples/window_functions.py b/examples/window_functions.py index e1f372b5..fa996f44 100644 --- a/examples/window_functions.py +++ b/examples/window_functions.py @@ -2,9 +2,12 @@ """Window Functions - Rolling Averages, Rankings, Running Totals, and t.all().""" import ibis -import xorq.api as xo from ibis import _ +# CI runs this example with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + from boring_semantic_layer import to_semantic_table BASE_URL = "https://pub-a45a6a332b4646f2a6f44775695c64df.r2.dev" @@ -32,13 +35,13 @@ def main(): result = ( daily_stats.mutate( rolling_avg=lambda t: t.flight_count.mean().over( - xo.window(order_by=t.flight_date, preceding=6, following=0), + xibis.window(order_by=t.flight_date, preceding=6, following=0), ), - rank=lambda t: xo.dense_rank().over( - xo.window(order_by=xo.desc(t.flight_count)), + rank=lambda t: xibis.dense_rank().over( + xibis.window(order_by=xibis.desc(t.flight_count)), ), running_total=lambda t: t.flight_count.sum().over( - xo.window(order_by=t.flight_date), + xibis.window(order_by=t.flight_date), ), ) .order_by("flight_date") @@ -65,8 +68,8 @@ def main(): result = ( carrier_stats.mutate( - total_flights=lambda t: t.flight_count.sum().over(xo.window()), - percent_manual=lambda t: (t.flight_count / t.flight_count.sum().over(xo.window())) + total_flights=lambda t: t.flight_count.sum().over(xibis.window()), + percent_manual=lambda t: (t.flight_count / t.flight_count.sum().over(xibis.window())) * 100, ) .order_by(_.percent_manual.desc()) diff --git a/pyproject.toml b/pyproject.toml index df2965ee..5764a028 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "pyyaml>=6.0", "returns>=0.26.0", "toolz>=1.0.0", - "xorq>=0.3.25", ] urls = { Homepage = "https://github.com/boringdata/boring-semantic-layer/tree/main" } license = "MIT" @@ -26,7 +25,8 @@ bsl = "boring_semantic_layer.agents.cli:main" bsl = "boring_semantic_layer.serialization.tag_handler:bsl_tag_handler" [project.optional-dependencies] -examples = ["xorq[duckdb]>=0.3.4", "xorq", "duckdb<1.4"] +xorq = ["xorq>=0.3.31"] +examples = ["xorq[duckdb]>=0.3.31", "duckdb<1.4"] # Visualization backends viz-altair = ["altair>=5.0.0", "vl-convert-python>=1.0.0"] @@ -51,11 +51,22 @@ server = [ "uvicorn[standard]>=0.30.0", ] +# Baseline test environment for the no-xorq CI leg: pulled in via +# `uv sync --all-extras` so the suite has a duckdb backend (and pyarrow) +# without depending on xorq or the examples extra. +test-core = [ + "ibis-framework[duckdb]>=11.0.0", + "pytest", + "pandas>=2.3.0", + "pytest-asyncio", +] + dev = [ - "boring-semantic-layer[agent,mcp,server,examples,viz-altair,viz-plotly,viz-plotext]", + # LLM provider clients needed to test agent backends (not user-facing extras) "openai>=1.0.0", "langchain-openai>=0.3.0", "langchain-anthropic>=0.3.0", + # Developer tooling "ruff>=0.6.7", "pre-commit>=4.2.0", "pytest", diff --git a/requirements-dev.txt b/requirements-dev.txt index e9df4081..fff7a143 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,8 @@ absl-py==2.3.1 # via malloy altair==5.5.0 # via boring-semantic-layer +annotated-doc==0.0.4 + # via fastapi annotated-types==0.7.0 # via pydantic anthropic==0.75.0 @@ -16,8 +18,11 @@ anyio==4.11.0 # openai # sse-starlette # starlette + # watchfiles asn1crypto==1.5.1 # via snowflake-connector-python +asttokens==3.0.1 + # via stack-data atpublic==6.0.2 # via # ibis-framework @@ -70,10 +75,11 @@ choreographer==1.2.0 # via kaleido cityhash==0.4.10 ; python_full_version < '4' # via xorq -click==8.3.0 ; python_full_version < '4' or sys_platform != 'emscripten' +click==8.3.0 # via # dask # uvicorn + # xorq cloudpickle==3.1.2 # via # dask @@ -81,8 +87,10 @@ cloudpickle==3.1.2 colorama==0.4.6 ; sys_platform == 'win32' # via # click + # ipython # pytest # tqdm + # uvicorn cryptography==46.0.3 # via # authlib @@ -95,6 +103,8 @@ cyclopts==4.2.1 # via fastmcp dask==2025.1.0 ; python_full_version < '4' # via xorq +decorator==5.2.1 + # via ipython diskcache==5.6.3 # via py-key-value-aio distlib==0.4.0 @@ -114,6 +124,7 @@ docutils==0.22.2 duckdb==1.3.2 # via # boring-semantic-layer + # ibis-framework # malloy # xorq email-validator==2.3.0 @@ -124,7 +135,12 @@ exceptiongroup==1.3.0 # via # anyio # fastmcp + # ipython # pytest +executing==2.2.1 + # via stack-data +fastapi==0.135.3 + # via boring-semantic-layer fastjsonschema==2.21.2 # via nbformat fastmcp==2.13.0.2 @@ -133,6 +149,7 @@ filelock==3.20.0 # via # snowflake-connector-python # virtualenv + # xorq fsspec==2025.10.0 ; python_full_version < '4' # via dask gast==0.6.0 ; sys_platform == 'darwin' @@ -141,6 +158,12 @@ gast==0.6.0 ; sys_platform == 'darwin' # pythran geoarrow-types==0.3.0 ; python_full_version < '4' # via xorq +git-annex==10.20260316 + # via xorq +gitdb==4.0.12 + # via gitpython +gitpython==3.1.46 + # via xorq google-api-core==2.28.1 # via # google-cloud-bigquery @@ -177,6 +200,8 @@ h11==0.16.0 # uvicorn httpcore==1.0.9 # via httpx +httptools==0.7.1 + # via uvicorn httpx==0.28.1 # via # anthropic @@ -205,12 +230,18 @@ importlib-metadata==8.7.0 # opentelemetry-api iniconfig==2.3.0 # via pytest +ipython==8.38.0 ; python_full_version < '3.11' +ipython==9.10.0 ; python_full_version >= '3.11' +ipython-pygments-lexers==1.1.1 ; python_full_version >= '3.11' + # via ipython jaraco-classes==3.4.0 # via keyring jaraco-context==6.0.1 # via keyring jaraco-functools==4.3.0 # via keyring +jedi==0.19.2 + # via ipython jeepney==0.9.0 ; sys_platform == 'linux' # via # keyring @@ -270,6 +301,8 @@ langgraph-sdk==0.2.10 # via langgraph langsmith==0.4.49 # via langchain-core +linkify-it-py==2.1.0 + # via markdown-it-py locket==1.0.0 ; python_full_version < '4' # via partd logistro==2.0.1 @@ -279,11 +312,18 @@ logistro==2.0.1 malloy==2024.1096 # via boring-semantic-layer markdown-it-py==4.0.0 - # via rich + # via + # mdit-py-plugins + # rich + # textual markupsafe==3.0.3 # via jinja2 +matplotlib-inline==0.2.1 + # via ipython mcp==1.20.0 # via fastmcp +mdit-py-plugins==0.5.0 + # via textual mdurl==0.1.2 # via markdown-it-py more-itertools==10.8.0 @@ -300,10 +340,12 @@ nodeenv==1.9.1 # via pre-commit numpy==2.2.6 ; python_full_version < '3.11' # via + # ibis-framework # pandas # pythran numpy==2.3.4 ; python_full_version >= '3.11' # via + # ibis-framework # pandas # pythran openai==2.8.1 @@ -326,7 +368,9 @@ opentelemetry-exporter-otlp-proto-common==1.38.0 # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp-proto-grpc==1.38.0 - # via opentelemetry-exporter-otlp + # via + # opentelemetry-exporter-otlp + # xorq opentelemetry-exporter-otlp-proto-http==1.38.0 # via opentelemetry-exporter-otlp opentelemetry-exporter-prometheus==0.59b0 @@ -357,6 +401,7 @@ packaging==25.0 # boring-semantic-layer # dask # google-cloud-bigquery + # ibis-framework # kaleido # langchain-core # langsmith @@ -366,7 +411,10 @@ packaging==25.0 pandas==2.3.3 # via # boring-semantic-layer + # ibis-framework # xorq +parso==0.8.6 + # via jedi parsy==2.2 # via # ibis-framework @@ -377,11 +425,14 @@ pathable==0.4.4 # via jsonschema-path pathvalidate==3.3.1 # via py-key-value-aio +pexpect==4.9.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' + # via ipython platformdirs==4.5.0 # via # fastmcp # jupyter-core # snowflake-connector-python + # textual # virtualenv plotext==5.3.2 # via boring-semantic-layer @@ -397,6 +448,8 @@ prometheus-client==0.23.1 # via # opentelemetry-exporter-prometheus # xorq +prompt-toolkit==3.0.52 + # via ipython proto-plus==1.26.1 # via google-api-core protobuf==6.33.0 @@ -406,16 +459,25 @@ protobuf==6.33.0 # grpcio-status # opentelemetry-proto # proto-plus +ptyprocess==0.7.0 ; sys_platform != 'emscripten' and sys_platform != 'win32' + # via pexpect +pure-eval==0.2.3 + # via stack-data py-key-value-aio==0.2.8 # via fastmcp py-key-value-shared==0.2.8 # via py-key-value-aio -pyarrow==21.0.0 ; python_full_version < '4' +py-yaml12==0.1.0 + # via xorq +pyarrow==21.0.0 # via + # ibis-framework # xorq # xorq-datafusion -pyarrow-hotfix==0.7 ; python_full_version < '4' - # via xorq +pyarrow-hotfix==0.7 + # via + # ibis-framework + # xorq pyasn1==0.6.1 # via # pyasn1-modules @@ -427,6 +489,7 @@ pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementati pydantic==2.12.3 # via # anthropic + # fastapi # fastmcp # langchain # langchain-anthropic @@ -443,8 +506,11 @@ pydantic-settings==2.11.0 # via mcp pygments==2.19.2 # via + # ipython + # ipython-pygments-lexers # pytest # rich + # textual pyjwt==2.10.1 # via # mcp @@ -457,12 +523,9 @@ pytest==8.4.2 # via # boring-semantic-layer # pytest-asyncio - # pytest-mock # pytest-timeout pytest-asyncio==1.2.0 # via boring-semantic-layer -pytest-mock==3.15.1 ; python_full_version < '4' - # via xorq pytest-timeout==2.4.0 # via kaleido python-dateutil==2.9.0.post0 @@ -477,6 +540,7 @@ python-dotenv==1.2.1 # boring-semantic-layer # fastmcp # pydantic-settings + # uvicorn python-multipart==0.0.20 # via mcp pythran==0.18.0 ; sys_platform == 'darwin' @@ -498,7 +562,7 @@ pyyaml==6.0.3 # jsonschema-path # langchain-core # pre-commit - # xorq + # uvicorn referencing==0.36.2 # via # jsonschema @@ -525,7 +589,9 @@ rich==14.2.0 # boring-semantic-layer # cyclopts # fastmcp + # ibis-framework # rich-rst + # textual # xorq rich-rst==1.3.2 # via cyclopts @@ -547,6 +613,8 @@ simplejson==3.20.2 # via choreographer six==1.17.0 # via python-dateutil +smmap==5.0.2 + # via gitdb sniffio==1.3.1 # via # anthropic @@ -562,14 +630,20 @@ sqlglot==25.20.2 # xorq sse-starlette==3.0.3 # via mcp +stack-data==0.6.3 + # via ipython starlette==0.50.0 - # via mcp + # via + # fastapi + # mcp strenum==0.4.15 ; python_full_version < '3.11' # via xorq structlog==25.5.0 ; python_full_version < '4' # via xorq tenacity==9.1.2 # via langchain-core +textual==8.1.0 + # via xorq tiktoken==0.12.0 # via langchain-openai tomli==2.3.0 ; python_full_version < '3.11' @@ -577,7 +651,9 @@ tomli==2.3.0 ; python_full_version < '3.11' # cyclopts # pytest tomlkit==0.13.3 - # via snowflake-connector-python + # via + # snowflake-connector-python + # xorq toolz==1.1.0 # via # boring-semantic-layer @@ -589,7 +665,9 @@ tqdm==4.67.1 # via openai traitlets==5.14.3 # via + # ipython # jupyter-core + # matplotlib-inline # nbformat typing-extensions==4.15.0 # via @@ -599,8 +677,10 @@ typing-extensions==4.15.0 # cryptography # cyclopts # exceptiongroup + # fastapi # grpcio # ibis-framework + # ipython # langchain-core # openai # opentelemetry-api @@ -618,18 +698,22 @@ typing-extensions==4.15.0 # snowflake-connector-python # starlette # structlog + # textual # typing-inspection # uvicorn # virtualenv # xorq typing-inspection==0.4.2 # via + # fastapi # pydantic # pydantic-settings tzdata==2025.2 # via # ibis-framework # pandas +uc-micro-py==2.0.0 + # via linkify-it-py urllib3==2.5.0 # via # boring-semantic-layer @@ -637,17 +721,27 @@ urllib3==2.5.0 # requests uv==0.9.7 # via xorq -uvicorn==0.38.0 ; sys_platform != 'emscripten' - # via mcp +uvicorn==0.38.0 + # via + # boring-semantic-layer + # mcp +uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' + # via uvicorn virtualenv==20.35.4 # via pre-commit vl-convert-python==1.8.0 # via boring-semantic-layer +watchfiles==1.1.1 + # via uvicorn +wcwidth==0.6.0 + # via prompt-toolkit websockets==15.0.1 - # via fastmcp -xorq==0.3.5 + # via + # fastmcp + # uvicorn +xorq==0.3.25 # via boring-semantic-layer -xorq-datafusion==0.2.4 +xorq-datafusion==0.2.7 # via xorq xxhash==3.6.0 # via langgraph diff --git a/scripts/demo_bsl_v2.py b/scripts/demo_bsl_v2.py index 4f06949a..b65815ef 100644 --- a/scripts/demo_bsl_v2.py +++ b/scripts/demo_bsl_v2.py @@ -10,17 +10,20 @@ - Rolling-window calculations """ -from pathlib import Path import sys +from pathlib import Path import ibis -import xorq.api as xo if __package__ in {None, ""}: sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from boring_semantic_layer import to_semantic_table +# CI runs this demo with and without xorq. xibis matches BSL's active ibis +# flavor in both modes; users who are not using xorq can simply use `import ibis`. +from boring_semantic_layer._xorq import ibis as xibis + def main(): # Setup in-memory DuckDB @@ -117,7 +120,7 @@ def main(): .with_measures(sum_val=lambda t: t.value.sum()) ) - rolling_window = xo.window(order_by="date", preceding=1, following=1) + rolling_window = xibis.window(order_by="date", preceding=1, following=1) expr4 = ( ts_st.group_by("date") .aggregate("sum_val") diff --git a/src/boring_semantic_layer/_xorq.py b/src/boring_semantic_layer/_xorq.py index 167d714b..20729417 100644 --- a/src/boring_semantic_layer/_xorq.py +++ b/src/boring_semantic_layer/_xorq.py @@ -6,52 +6,172 @@ This shim does NOT replace the plain ``ibis`` package (PyPI ibis-framework). BSL coexists with both flavors: use ``import ibis`` for the plain side, and this module for the ``xorq.vendor.ibis`` side. + +When xorq is not installed, this shim falls back to plain ``ibis-framework`` +equivalents. Xorq-only features (``to_tagged``, ``from_tagged``, tagging, +caching) remain gated behind explicit ``ImportError`` checks in their +respective modules. """ from __future__ import annotations -import xorq.api as api -from xorq.api import selectors -from xorq.common.utils.graph_utils import to_node -from xorq.common.utils.ibis_utils import from_ibis, map_ibis -from xorq.common.utils.node_utils import replace_nodes, walk_nodes -from xorq.expr.builders import TagHandler -from xorq.expr.relations import CachedNode, Read, RemoteTable, Tag -from xorq.vendor import ibis -from xorq.vendor.ibis import _ -from xorq.vendor.ibis.backends.profiles import Profile -from xorq.vendor.ibis.common.collections import FrozenDict, FrozenOrderedDict -from xorq.vendor.ibis.common.deferred import ( - Attr, - BinaryOperator, - Call, - Deferred, - Item, - Just, - JustUnhashable, - Mapping, - Sequence, - UnaryOperator, - Variable, -) -from xorq.vendor.ibis.common.graph import Graph -from xorq.vendor.ibis.config import Config -from xorq.vendor.ibis.expr import operations, types -from xorq.vendor.ibis.expr.format import fmt, render_fields -from xorq.vendor.ibis.expr.operations import relations -from xorq.vendor.ibis.expr.operations.core import Node -from xorq.vendor.ibis.expr.operations.generic import Literal -from xorq.vendor.ibis.expr.operations.relations import ( - DatabaseTable, - Field, - JoinChain, - JoinReference, -) -from xorq.vendor.ibis.expr.operations.sortkeys import SortKey -from xorq.vendor.ibis.expr.schema import Schema -from xorq.vendor.ibis.expr.types import Expr, Table -from xorq.vendor.ibis.expr.types.generic import Column -from xorq.vendor.ibis.expr.types.groupby import GroupedTable +try: + import xorq.api as api + from xorq.api import selectors + from xorq.common.utils.graph_utils import to_node + from xorq.common.utils.ibis_utils import from_ibis, map_ibis + from xorq.common.utils.node_utils import replace_nodes, walk_nodes + from xorq.expr.builders import TagHandler + from xorq.expr.relations import CachedNode, Read, RemoteTable, Tag + from xorq.vendor import ibis + from xorq.vendor.ibis import _ + from xorq.vendor.ibis.backends.profiles import Profile + from xorq.vendor.ibis.common.collections import FrozenDict, FrozenOrderedDict + from xorq.vendor.ibis.common.deferred import ( + Attr, + BinaryOperator, + Call, + Deferred, + Item, + Just, + JustUnhashable, + Mapping, + Sequence, + UnaryOperator, + Variable, + ) + from xorq.vendor.ibis.common.graph import Graph + from xorq.vendor.ibis.config import Config + from xorq.vendor.ibis.expr import operations, types + from xorq.vendor.ibis.expr.format import fmt, render_fields + from xorq.vendor.ibis.expr.operations import relations + from xorq.vendor.ibis.expr.operations.core import Node + from xorq.vendor.ibis.expr.operations.generic import Literal + from xorq.vendor.ibis.expr.operations.relations import ( + DatabaseTable, + Field, + JoinChain, + JoinReference, + ) + from xorq.vendor.ibis.expr.operations.sortkeys import SortKey + from xorq.vendor.ibis.expr.schema import Schema + from xorq.vendor.ibis.expr.types import Expr, Table + from xorq.vendor.ibis.expr.types.generic import Column + from xorq.vendor.ibis.expr.types.groupby import GroupedTable + + HAS_XORQ = True + +except ImportError: + import ibis + from ibis import selectors + from ibis.common.collections import FrozenDict, FrozenOrderedDict + from ibis.common.deferred import ( + Attr, + BinaryOperator, + Call, + Deferred, + Item, + Just, + JustUnhashable, + Mapping, + Sequence, + UnaryOperator, + Variable, + ) + from ibis.common.graph import Graph + from ibis.config import Config + from ibis.expr import operations, types + from ibis.expr.format import fmt, render_fields + from ibis.expr.operations import relations + from ibis.expr.operations.core import Node + from ibis.expr.operations.generic import Literal + from ibis.expr.operations.relations import ( + DatabaseTable, + Field, + JoinChain, + JoinReference, + ) + from ibis.expr.operations.sortkeys import SortKey + from ibis.expr.schema import Schema + from ibis.expr.types import Expr, Table + from ibis.expr.types.generic import Column + from ibis.expr.types.groupby import GroupedTable + + api = ibis + _ = ibis._ + + HAS_XORQ = False + + # Xorq-only symbols: None sentinels so isinstance checks return False and + # call sites that are already gated (to_tagged, from_tagged, tag_handler) + # will fail with a clear AttributeError rather than a confusing NameError. + TagHandler = None + CachedNode = None + Read = None + RemoteTable = None + Tag = None + Profile = None + + def from_ibis(table): + """Identity: without xorq, plain-ibis tables stay as plain ibis.""" + return table + + class _MapIbisStub: + """Stub for xorq's map_ibis singledispatch mechanism. + + Allows _patch_xorq_sortkey_compat() to register a handler without + error; the handler body never runs because map_ibis() is only called + by xorq internals during xorq-table conversion, which doesn't happen + when xorq is absent. + """ + + def __init__(self): + self.registry = {} + + def register(self, type_): + def decorator(fn): + self.registry[type_] = fn + return fn + + return decorator + + def __call__(self, *args, **kwargs): + raise ImportError( + "xorq is required for map_ibis; " + "install with: pip install 'boring-semantic-layer[xorq]'" + ) + + map_ibis = _MapIbisStub() + + def to_node(maybe_expr): + """Convert an expression or node to a Node.""" + if isinstance(maybe_expr, Node): + return maybe_expr + if isinstance(maybe_expr, Expr): + return maybe_expr.op() + raise ValueError(f"Cannot convert {type(maybe_expr).__name__!r} to an expression node") + + def replace_nodes(replacer, node): + """Replace nodes in the tree via ibis ``Node.replace``. + + The xorq replacer signature is ``(node, kwargs) -> node``; ibis passes + ``None`` for kwargs when no children changed, which we normalise to + ``{}`` to match xorq's contract. + """ + return node.replace(lambda n, kwargs: replacer(n, kwargs if kwargs is not None else {})) + + def walk_nodes(*args, **kwargs): + # Defined so the shim's symbol surface stays symmetric: every name the + # xorq branch exports is importable from this branch too, so the + # function-local ``from ._xorq import walk_nodes`` at xorq-gated call + # sites resolves. Those call sites all bail on ``HAS_XORQ`` first, so the + # body never runs without xorq; the raise is a clear signal if a future + # un-gated caller ever reaches it. + raise ImportError( + "xorq is required for walk_nodes; " + "install with: pip install 'boring-semantic-layer[xorq]'" + ) + __all__ = [ "Attr", @@ -68,6 +188,7 @@ "FrozenOrderedDict", "Graph", "GroupedTable", + "HAS_XORQ", "Item", "JoinChain", "JoinReference", diff --git a/src/boring_semantic_layer/chart/tests/test_chart.py b/src/boring_semantic_layer/chart/tests/test_chart.py index 6d3d37f8..ea01aeef 100644 --- a/src/boring_semantic_layer/chart/tests/test_chart.py +++ b/src/boring_semantic_layer/chart/tests/test_chart.py @@ -3,10 +3,13 @@ import ibis import pandas as pd import pytest -import xorq.api as xo from boring_semantic_layer import to_semantic_table +# Match BSL's active ibis flavor: plain ibis without xorq, xorq-vendored ibis +# with xorq installed. Plain-ibis users can write `import ibis` directly. +from boring_semantic_layer._xorq import ibis as xibis + @pytest.fixture(scope="module") def con(): @@ -387,7 +390,7 @@ def test_chart_with_dynamic_dimension_and_rolling_window(self, flights_model): .order_by("flight_week") .mutate( rolling_avg=lambda t: t.flight_count.mean().over( - xo.window(rows=(-2, 0), order_by="flight_week") + xibis.window(rows=(-2, 0), order_by="flight_week") ) ) ) diff --git a/src/boring_semantic_layer/graph_utils.py b/src/boring_semantic_layer/graph_utils.py index d7f50ec3..4607ef0c 100644 --- a/src/boring_semantic_layer/graph_utils.py +++ b/src/boring_semantic_layer/graph_utils.py @@ -2,15 +2,12 @@ from collections.abc import Callable, Sequence from functools import reduce as functools_reduce -from operator import methodcaller from typing import Any from ibis.expr.operations.core import Node as IbisNode from ibis.expr.types import Expr as IbisExpr -from returns.curry import partial from returns.maybe import Maybe, Nothing, Some from returns.result import Result, Success, safe -from toolz import compose from ._xorq import ( Expr as XorqExpr, @@ -159,12 +156,21 @@ def walk_nodes(node_types, expr): def replace_nodes(replacer, expr): - """Replace nodes in expression tree using functional composition.""" - return compose( - methodcaller("to_expr"), - partial(_xorq_replace_nodes, replacer), - to_node, - )(expr) + """Replace nodes in an expression tree. + + xorq's ``replace_nodes`` only understands xorq's vendored-ibis nodes and + raises on plain-ibis ones. Since BSL coexists with both flavors, dispatch + plain-ibis nodes to ibis's native ``Node.replace`` (normalising the + ``None`` kwargs ibis passes for unchanged children to ``{}`` to match the + xorq replacer contract) and xorq nodes to xorq's ``replace_nodes``. + """ + node = to_node(expr) + if isinstance(node, IbisNode): + new_node = node.replace( + lambda n, kwargs: replacer(n, kwargs if kwargs is not None else {}) + ) + return new_node.to_expr() + return _xorq_replace_nodes(replacer, node).to_expr() @safe(exceptions=(ValueError,)) diff --git a/src/boring_semantic_layer/ops.py b/src/boring_semantic_layer/ops.py index 77c205c5..bb9fda16 100644 --- a/src/boring_semantic_layer/ops.py +++ b/src/boring_semantic_layer/ops.py @@ -315,6 +315,12 @@ def _rebind_to_canonical_backend(expr): No-op on plain ibis expressions (not xorq-vendored). """ + from ._xorq import HAS_XORQ + + # Without xorq there is only one backend, so there is nothing to rebind. + if not HAS_XORQ: + return expr + try: from ._xorq import relations as xorq_rel, walk_nodes except Exception: @@ -734,6 +740,16 @@ def _classify_measure( if prefer_known is None: prefer_known = getattr(scope, "_prefer_known", ()) prefer_known_set = frozenset(prefer_known) + try: + # Use object.__getattribute__ so ibis Deferred.__getattr__ does not + # synthesize a resolver for this private marker. + expr_prefer_known = object.__getattribute__(expr, "__bsl_prefer_known__") + except AttributeError: + expr_prefer_known = () + if expr_prefer_known is True: + prefer_known_set = prefer_known_set | known_set + else: + prefer_known_set = prefer_known_set | frozenset(expr_prefer_known or ()) # Pure constants fold into both grouped and ungrouped contexts. if isinstance(expr, (int, float)) and not isinstance(expr, bool): @@ -4332,6 +4348,13 @@ def _rebind_join_backends(left_tbl, right_tbl): fall back to returning the inputs unchanged so ibis executes the join natively. Rebinding is only needed for xorq-vendored backends. """ + from ._xorq import HAS_XORQ + + # Without xorq, from_ibis() is an identity, so both sides already share + # their backends — nothing to rebind (see _rebind_to_canonical_backend). + if not HAS_XORQ: + return left_tbl, right_tbl + try: from ._xorq import relations as xorq_rel, walk_nodes except ImportError: @@ -4884,6 +4907,12 @@ def _dimension_only_source_table( tbl_cols = frozenset(tbl.columns) | frozenset(root_dims) for flt in filters: fn = _unwrap(flt) if hasattr(flt, "unwrap") else flt + # Dict/string filters resolve deferred through the + # backend; their columns can't be statically + # introspected, so disable the shortcut rather than + # risk a wrong source table. See query.Filter.to_callable. + if getattr(fn, "__bsl_deferred_resolution__", False): + return None extraction = _extract_columns_from_callable(fn, tbl) if extraction.extraction_failed: return None # Can't determine — bail out diff --git a/src/boring_semantic_layer/profile.py b/src/boring_semantic_layer/profile.py index 5d65c709..b48cfb8f 100644 --- a/src/boring_semantic_layer/profile.py +++ b/src/boring_semantic_layer/profile.py @@ -1,10 +1,12 @@ from __future__ import annotations import os +import re from pathlib import Path from ibis import BaseBackend -from ._xorq import Profile as XorqProfile + +from ._xorq import HAS_XORQ, Profile as XorqProfile from .utils import read_yaml_file @@ -83,7 +85,7 @@ def _search_profile(name: str, search_locations: list[str]) -> BaseBackend: return _load_from_file(bsl_profile, name) if location == "local" and local_profile.exists(): return _load_from_file(local_profile, name) - if location == "xorq_dir": + if location == "xorq_dir" and HAS_XORQ: try: return XorqProfile.load(name).get_con() except Exception: @@ -112,12 +114,50 @@ def _load_from_file(yaml_file: Path, profile_name: str | None = None) -> BaseBac return _create_connection_from_config(config) +_ENV_VAR_PATTERN = re.compile(r"\$\{(\w+)\}|\$(\w+)") + + +def _expand_env_vars(value: str) -> str: + """Expand ``${VAR}``/``$VAR`` references, raising on undefined variables. + + Unlike ``os.path.expandvars``, which silently leaves unresolved + references in place (so ``database: ${MISSING}`` would create a file + literally named ``${MISSING}``), this mirrors xorq's strict behavior and + fails loudly. Keeps env-var handling consistent whether or not xorq is + installed. + """ + + def _replace(match: re.Match) -> str: + name = match.group(1) or match.group(2) + try: + return os.environ[name] + except KeyError: + raise ProfileError( + f"Environment variable '{name}' referenced in profile is not set." + ) from None + + return _ENV_VAR_PATTERN.sub(_replace, value) + + +def _connect_plain_ibis(ibis, config: dict, conn_type: str) -> BaseBackend: + """Connect using plain ibis..connect() with manual env-var expansion.""" + connect_fn = getattr(ibis, conn_type, None) + if connect_fn is None or not callable(getattr(connect_fn, "connect", None)): + raise ProfileError(f"Unknown backend type: '{conn_type}'") + connect_kwargs = {k: v for k, v in config.items() if k != "type"} + connect_kwargs = { + k: _expand_env_vars(v) if isinstance(v, str) else v + for k, v in connect_kwargs.items() + } + return connect_fn.connect(**connect_kwargs) + + def _create_connection_from_config(config: dict) -> BaseBackend: """Create database connection from config dict with 'type' field. - Tries xorq first (which handles env var substitution automatically), - then falls back to plain ``ibis..connect()`` for backends - not registered in xorq (e.g. Databricks). + When xorq is installed, tries xorq first (handles env-var substitution + automatically) and falls back to plain ibis for unsupported backends. + When xorq is absent, uses plain ``ibis..connect()`` directly. """ import ibis @@ -128,23 +168,16 @@ def _create_connection_from_config(config: dict) -> BaseBackend: parquet_tables = config.pop("tables", None) - # Try xorq first (handles env var substitution automatically) - try: - kwargs_tuple = tuple(sorted((k, v) for k, v in config.items() if k != "type")) - xorq_profile = XorqProfile(con_name=conn_type, kwargs_tuple=kwargs_tuple) - connection = xorq_profile.get_con() - except AssertionError: - # Backend not supported by xorq (e.g. Databricks) — fall back to plain ibis - connect_fn = getattr(ibis, conn_type, None) - if connect_fn is None or not callable(getattr(connect_fn, "connect", None)): - raise ProfileError(f"Unknown backend type: '{conn_type}'") - connect_kwargs = {k: v for k, v in config.items() if k != "type"} - # Expand env vars in string values (xorq does this automatically) - connect_kwargs = { - k: os.path.expandvars(v) if isinstance(v, str) else v - for k, v in connect_kwargs.items() - } - connection = connect_fn.connect(**connect_kwargs) + if HAS_XORQ: + # Try xorq first (handles env var substitution automatically) + try: + kwargs_tuple = tuple(sorted((k, v) for k, v in config.items() if k != "type")) + xorq_profile = XorqProfile(con_name=conn_type, kwargs_tuple=kwargs_tuple) + connection = xorq_profile.get_con() + except AssertionError: + connection = _connect_plain_ibis(ibis, config, conn_type) + else: + connection = _connect_plain_ibis(ibis, config, conn_type) # Load parquet tables if specified if parquet_tables: diff --git a/src/boring_semantic_layer/query.py b/src/boring_semantic_layer/query.py index 360c4b96..22ab522c 100644 --- a/src/boring_semantic_layer/query.py +++ b/src/boring_semantic_layer/query.py @@ -206,18 +206,32 @@ def to_callable(self) -> Callable: if isinstance(self.filter, dict): pred = pred_mod.from_dict(self.filter) ibis_module = _get_ibis_api() - return lambda t: pred_mod.compile( - pred, - ibis_module._, - ibis_module=ibis_module, - ).resolve(_ensure_xorq_table(t)) + + def _dict_filter(t): + return pred_mod.compile( + pred, + ibis_module._, + ibis_module=ibis_module, + ).resolve(_ensure_xorq_table(t)) + + # Deferred resolution: columns can't be statically introspected + # (see ops._dimension_only_source_table). Marked so callers can opt + # out of static-column optimizations consistently regardless of + # whether xorq is installed. + _dict_filter.__bsl_deferred_resolution__ = True + return _dict_filter elif isinstance(self.filter, str): _ibis = _get_ibis_api() expr = safe_eval( self.filter, context={"_": _ibis._, "ibis": _ibis}, ).unwrap() - return lambda t: expr.resolve(_ensure_xorq_table(t)) + + def _str_filter(t): + return expr.resolve(_ensure_xorq_table(t)) + + _str_filter.__bsl_deferred_resolution__ = True + return _str_filter elif callable(self.filter): return self.filter raise ValueError("Filter must be a dict, string, or callable") diff --git a/src/boring_semantic_layer/serialization/__init__.py b/src/boring_semantic_layer/serialization/__init__.py index ee3baa21..4ccccbc5 100644 --- a/src/boring_semantic_layer/serialization/__init__.py +++ b/src/boring_semantic_layer/serialization/__init__.py @@ -156,8 +156,19 @@ def from_tagged(tagged_expr, context: BSLSerializationContext | None = None): BSL expression reconstructed from metadata Raises: + ImportError: If xorq is not installed ValueError: If no BSL metadata found in expression """ + result = try_import_xorq() + if isinstance(result, Failure): + error = result.failure() + if isinstance(error, ImportError): + raise ImportError( + "Xorq conversion requires the 'xorq' optional dependency. " + "Install with: pip install 'boring-semantic-layer[xorq]'" + ) from error + raise error + if context is None: context = BSLSerializationContext() diff --git a/src/boring_semantic_layer/serialization/tag_handler.py b/src/boring_semantic_layer/serialization/tag_handler.py index e141b249..2b050336 100644 --- a/src/boring_semantic_layer/serialization/tag_handler.py +++ b/src/boring_semantic_layer/serialization/tag_handler.py @@ -16,7 +16,7 @@ from typing import Any -from .._xorq import TagHandler +from .._xorq import HAS_XORQ, TagHandler from . import ( BSLSerializationContext, @@ -117,15 +117,17 @@ def reemit(tag_node, rebuild_subexpr): return new_source.hashing_tag(tag=tag_name, **meta) -_handler_kwargs = dict( - tag_names=("bsl",), - extract_metadata=extract_metadata, - from_tag_node=from_tag_node, -) -if "reemit" in {a.name for a in TagHandler.__attrs_attrs__}: - _handler_kwargs["reemit"] = reemit - -bsl_tag_handler = TagHandler(**_handler_kwargs) +if HAS_XORQ: + _handler_kwargs = dict( + tag_names=("bsl",), + extract_metadata=extract_metadata, + from_tag_node=from_tag_node, + ) + if "reemit" in {a.name for a in TagHandler.__attrs_attrs__}: + _handler_kwargs["reemit"] = reemit + bsl_tag_handler = TagHandler(**_handler_kwargs) +else: + bsl_tag_handler = None __all__ = [ diff --git a/src/boring_semantic_layer/tests/integration/conftest.py b/src/boring_semantic_layer/tests/integration/conftest.py index 902508f6..c0696388 100644 --- a/src/boring_semantic_layer/tests/integration/conftest.py +++ b/src/boring_semantic_layer/tests/integration/conftest.py @@ -13,6 +13,16 @@ import pytest from toolz import curry +# Several Malloy comparison query modules under ``malloy_data/*.py`` import +# ``xorq.api`` directly, and the parametrized integration suite includes those +# modules. Skip collecting the integration directory when xorq is absent (the +# no-xorq CI leg) rather than relying on a ``--ignore`` CLI flag, so the gate +# lives with the tests that need it. +try: + import xorq # noqa: F401 +except ImportError: + collect_ignore_glob = ["*"] + TEST_CASES: tuple[tuple[str, str, tuple[str, ...]], ...] = ( ("comparing_timeframe", "query_1", ()), ("comparing_timeframe", "query_2", ()), diff --git a/src/boring_semantic_layer/tests/test_config_projection_pushdown.py b/src/boring_semantic_layer/tests/test_config_projection_pushdown.py index d3942bfa..bca9e95d 100644 --- a/src/boring_semantic_layer/tests/test_config_projection_pushdown.py +++ b/src/boring_semantic_layer/tests/test_config_projection_pushdown.py @@ -15,9 +15,12 @@ from boring_semantic_layer import to_semantic_table, to_untagged -# Projection pushdown disabled for xorq compatibility +# Projection pushdown is disabled in BSL (for xorq-vendored ibis compatibility), +# so these tests xfail regardless of whether xorq is installed. A few incidentally +# xpass under plain ibis where the emitted SQL happens to match; that is not the +# feature working, so the marker is intentionally unconditional and non-strict. pytestmark = pytest.mark.xfail( - reason="Projection pushdown disabled for xorq vendored ibis compatibility" + reason="Projection pushdown disabled for xorq-vendored ibis compatibility" ) diff --git a/src/boring_semantic_layer/tests/test_date_filter_fix.py b/src/boring_semantic_layer/tests/test_date_filter_fix.py index 314f92de..9213440a 100644 --- a/src/boring_semantic_layer/tests/test_date_filter_fix.py +++ b/src/boring_semantic_layer/tests/test_date_filter_fix.py @@ -9,6 +9,10 @@ import ibis import pytest +# BSL builds filter expressions with xorq's vendored ibis when xorq is +# installed; use the same flavor (plain ibis otherwise) so to_sql can compile +# the expression that Filter.to_callable produced. +from boring_semantic_layer._xorq import ibis as xibis from boring_semantic_layer.api import to_semantic_table from boring_semantic_layer.query import Filter, query @@ -220,10 +224,20 @@ def test_numeric_values_unchanged(self, orders_table): class TestSQLGeneration: """Test SQL generation for different backends.""" - def test_trino_sql_generation(self): - """Test that Trino/Athena SQL uses proper date functions.""" - from xorq.vendor import ibis as xibis - + @pytest.mark.parametrize( + "dialect, expected_fns", + [ + ("trino", ("FROM_ISO8601_TIMESTAMP",)), + ("duckdb", ("MAKE_TIMESTAMP", "MAKE_DATE")), + ], + ) + def test_sql_generation_uses_typed_date_functions(self, dialect, expected_fns): + """Generated SQL uses proper typed date functions per dialect. + + Compiles the filter expression to a dialect string via ibis/xorq's + compiler (no live backend required) and asserts the emitted SQL uses a + typed date constructor rather than a raw string literal. + """ con = ibis.duckdb.connect(":memory:") t = con.create_table( "test", @@ -234,49 +248,20 @@ def test_trino_sql_generation(self): filter_obj = Filter(filter={"field": "date_col", "operator": ">=", "value": "2024-01-01"}) filtered = filter_obj.to_callable()(t) - sql = xibis.to_sql(filtered, dialect="trino") + sql = xibis.to_sql(filtered, dialect=dialect) - # Should contain Trino date function - assert "FROM_ISO8601_TIMESTAMP" in sql - - def test_duckdb_sql_generation(self): - """Test that DuckDB SQL uses proper date functions.""" - from xorq.vendor import ibis as xibis - - con = ibis.duckdb.connect(":memory:") - t = con.create_table( - "test", - {"date_col": ["2024-01-01", "2024-06-15"]}, - schema={"date_col": "date"}, - ) - - filter_obj = Filter(filter={"field": "date_col", "operator": ">=", "value": "2024-01-01"}) - filtered = filter_obj.to_callable()(t) - - sql = xibis.to_sql(filtered, dialect="duckdb") - - # Should contain DuckDB date function - assert "MAKE_TIMESTAMP" in sql or "MAKE_DATE" in sql + assert any(fn in sql for fn in expected_fns) class TestFilterValueConversion: """Test the _convert_filter_value method directly.""" - def test_convert_date_string(self): - """Test conversion of date string.""" - f = Filter(filter={"field": "x", "operator": "=", "value": "2024-01-01"}) - result = f._convert_filter_value("2024-01-01") - from xorq.vendor.ibis.expr.types.temporal import TimestampScalar as XTimestampScalar - - assert isinstance(result, (ibis.expr.types.temporal.TimestampScalar, XTimestampScalar)) - - def test_convert_timestamp_string(self): - """Test conversion of timestamp string.""" - from xorq.vendor.ibis.expr.types.temporal import TimestampScalar as XTimestampScalar - - f = Filter(filter={"field": "x", "operator": "=", "value": "2024-01-01"}) - result = f._convert_filter_value("2024-01-01T12:00:00") - assert isinstance(result, (ibis.expr.types.temporal.TimestampScalar, XTimestampScalar)) + @pytest.mark.parametrize("value", ["2024-01-01", "2024-01-01T12:00:00"]) + def test_convert_date_string_to_timestamp_literal(self, value): + """Date/timestamp strings convert to typed timestamp literals.""" + f = Filter(filter={"field": "x", "operator": "=", "value": value}) + result = f._convert_filter_value(value) + assert result.type().is_timestamp() def test_non_date_string_passthrough(self): """Test that non-date strings pass through unchanged.""" diff --git a/src/boring_semantic_layer/tests/test_dependency_groups.py b/src/boring_semantic_layer/tests/test_dependency_groups.py index bdfd2c0f..d743e5ee 100644 --- a/src/boring_semantic_layer/tests/test_dependency_groups.py +++ b/src/boring_semantic_layer/tests/test_dependency_groups.py @@ -5,16 +5,19 @@ they receive clear error messages indicating which dependency group to install. Dependency groups in pyproject.toml: +- xorq: For tagged-expression serialization (to_tagged/from_tagged), caching - mcp: For MCP semantic model functionality (MCPSemanticModel) - agent: For LangChain-based query agents - viz-altair: For Altair visualization (chart with backend="altair") - viz-plotly: For Plotly visualization (chart with backend="plotly") - examples: For running examples (includes xorq and duckdb) -Note: xorq is a core dependency (not optional) for window compatibility. +Note: xorq is an optional dependency; core BSL works without it. """ +import subprocess import sys +import textwrap from pathlib import Path import pytest @@ -46,6 +49,7 @@ def test_pyproject_has_all_optional_dependencies(self): optional_deps = pyproject["project"]["optional-dependencies"] # Verify all expected groups exist + assert "xorq" in optional_deps, "xorq dependency group missing" assert "mcp" in optional_deps, "mcp dependency group missing" assert "agent" in optional_deps, "agent dependency group missing" assert "viz-altair" in optional_deps, "viz-altair dependency group missing" @@ -53,6 +57,7 @@ def test_pyproject_has_all_optional_dependencies(self): assert "examples" in optional_deps, "examples dependency group missing" # Verify key dependencies in each group + assert any("xorq" in dep for dep in optional_deps["xorq"]) assert any("fastmcp" in dep for dep in optional_deps["mcp"]) assert any("langchain" in dep for dep in optional_deps["agent"]) assert any("altair" in dep for dep in optional_deps["viz-altair"]) @@ -60,7 +65,7 @@ def test_pyproject_has_all_optional_dependencies(self): assert any("xorq" in dep for dep in optional_deps["examples"]) def test_all_dependency_groups_in_dev(self): - """Verify dev dependency group includes all optional dependencies.""" + """Verify dev dependency group contains developer tooling (not a self-referential bundle).""" # Use tomllib (Python 3.11+) or tomli (Python 3.10) if sys.version_info >= (3, 11): import tomllib @@ -79,21 +84,19 @@ def test_all_dependency_groups_in_dev(self): dev_deps = pyproject["project"]["optional-dependencies"]["dev"] - # Dev should include all optional dependency groups - # Check for the pattern boring-semantic-layer[...] in dev deps - dev_with_extras = [dep for dep in dev_deps if "boring-semantic-layer[" in dep] + # Dev should NOT use a self-referential boring-semantic-layer[...] bundle; + # optional extras (xorq, mcp, etc.) are installed separately via --all-extras. + dev_with_self_ref = [dep for dep in dev_deps if "boring-semantic-layer[" in dep] + assert len(dev_with_self_ref) == 0, ( + "Dev should not use a self-referential bundle; install extras via --all-extras" + ) - assert len(dev_with_extras) > 0, "Dev should include boring-semantic-layer with extras" - - # The first dev dependency should include all the optional groups (not xorq, it's required) - if dev_with_extras: - first_dep = dev_with_extras[0] - assert "mcp" in first_dep - assert "examples" in first_dep - assert "agent" in first_dep - assert "viz-altair" in first_dep - assert "viz-plotly" in first_dep - assert "viz-plotext" in first_dep + # Dev should contain developer tooling + assert any("ruff" in dep for dep in dev_deps), "Dev should include ruff" + assert any("pre-commit" in dep for dep in dev_deps), "Dev should include pre-commit" + assert any("langchain-anthropic" in dep or "langchain-openai" in dep for dep in dev_deps), ( + "Dev should include LLM provider clients for testing" + ) class TestXorqErrorMessages: @@ -108,8 +111,61 @@ def test_serialization_module_has_error_handling(self): # Check that to_tagged raises ImportError with helpful message if xorq is not available source = inspect.getsource(serialization.to_tagged) assert "ImportError" in source - # xorq is now a core dependency, so the error message should be generic - assert "xorq" in source.lower() + # xorq is optional, so the message should name the install extra. + assert "boring-semantic-layer[xorq]" in source + + def test_tagged_api_errors_are_helpful_without_xorq(self): + """Verify tagged APIs fail cleanly when xorq is not installed.""" + test_file = Path(__file__) + project_root = test_file.parent.parent.parent.parent + code = textwrap.dedent( + """ + import importlib.abc + import sys + + sys.path.insert(0, "src") + + class BlockXorq(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "xorq" or fullname.startswith("xorq."): + raise ImportError("blocked xorq for optional dependency test") + return None + + sys.meta_path.insert(0, BlockXorq()) + + import ibis + + from boring_semantic_layer import to_semantic_table + from boring_semantic_layer._xorq import HAS_XORQ + from boring_semantic_layer.serialization import from_tagged + + assert HAS_XORQ is False + table = ibis.table({"a": "int64"}, name="t") + model = to_semantic_table(table, name="t").with_dimensions(a=lambda t: t.a) + + errors = [] + for call in (model.to_tagged, lambda: from_tagged(object())): + try: + call() + except ImportError as exc: + errors.append(str(exc)) + else: + raise AssertionError("tagged API unexpectedly succeeded without xorq") + + assert len(errors) == 2 + for message in errors: + assert "xorq" in message.lower() + assert "optional dependency" in message.lower() + assert "boring-semantic-layer[xorq]" in message + """ + ) + subprocess.run( + [sys.executable, "-c", code], + cwd=project_root, + text=True, + capture_output=True, + check=True, + ) class TestMCPErrorMessages: @@ -210,7 +266,7 @@ def test_all_features_with_optional_deps_documented(self): assert "viz-plotly" in test_file_content or "plotly" in test_file_content def test_pyproject_dev_group_is_comprehensive(self): - """Verify dev group in pyproject.toml includes all optional dependencies.""" + """Verify all optional dependency groups are declared in pyproject.toml.""" # Use tomllib (Python 3.11+) or tomli (Python 3.10) if sys.version_info >= (3, 11): import tomllib @@ -226,18 +282,14 @@ def test_pyproject_dev_group_is_comprehensive(self): with open(pyproject_path, "rb") as f: pyproject = tomllib.load(f) - # Get all optional dependency group names (excluding dev itself and examples) optional_deps = pyproject["project"]["optional-dependencies"] - optional_groups = [group for group in optional_deps if group not in ["dev", "examples"]] - - # Dev dependencies string - dev_deps_str = str(pyproject["project"]["optional-dependencies"]["dev"]) - # Check that dev group references all other optional groups - # It should have boring-semantic-layer[group1,group2,...] - for group in optional_groups: - assert group in dev_deps_str, ( - f"Dev group should include optional dependency group: {group}" + # All user-facing extras must exist as top-level optional-dependencies keys. + # (Extras are no longer bundled into dev; they are installed via --all-extras in CI.) + expected_extras = {"xorq", "mcp", "agent", "viz-altair", "viz-plotly", "viz-plotext"} + for group in expected_extras: + assert group in optional_deps, ( + f"Optional dependency group '{group}' missing from pyproject.toml" ) @@ -261,19 +313,19 @@ def test_xorq_available_if_installed(self): assert callable(to_tagged) assert callable(from_tagged) else: - # xorq not installed, verify we get helpful error - with pytest.raises((ImportError, AttributeError)) as exc_info: + # xorq not installed, verify we get helpful error with a + # schema-only table (avoids pandas/pyarrow dependency) + with pytest.raises(ImportError) as exc_info: import ibis from boring_semantic_layer import SemanticModel from boring_semantic_layer.serialization import to_tagged - table = ibis.memtable({"a": [1]}) + table = ibis.table({"a": "int64"}, "test") model = SemanticModel(table=table, dimensions={}, measures={}) to_tagged(model) - # Should mention xorq in the error - assert "xorq" in str(exc_info.value).lower() or "xorq" in str(exc_info.typename).lower() + assert "xorq" in str(exc_info.value).lower() def test_mcp_available_if_installed(self): """Verify MCPSemanticModel works when fastmcp is installed.""" diff --git a/src/boring_semantic_layer/tests/test_index.py b/src/boring_semantic_layer/tests/test_index.py index 27af3d4a..96480139 100644 --- a/src/boring_semantic_layer/tests/test_index.py +++ b/src/boring_semantic_layer/tests/test_index.py @@ -14,7 +14,9 @@ import ibis import pandas as pd import pytest -import xorq.api as xo + +xorq = pytest.importorskip("xorq", reason="xorq not installed") +import xorq.api as xo # noqa: E402 from boring_semantic_layer import to_semantic_table diff --git a/src/boring_semantic_layer/tests/test_malloy_inspired.py b/src/boring_semantic_layer/tests/test_malloy_inspired.py index 52ed0a6e..94e74949 100644 --- a/src/boring_semantic_layer/tests/test_malloy_inspired.py +++ b/src/boring_semantic_layer/tests/test_malloy_inspired.py @@ -20,7 +20,9 @@ import pandas as pd import pytest from ibis import _ -import xorq.api as xo + +xorq = pytest.importorskip("xorq", reason="xorq not installed") +import xorq.api as xo # noqa: E402 from boring_semantic_layer import to_semantic_table diff --git a/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py b/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py index 2f9366f4..793b48a1 100644 --- a/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py +++ b/src/boring_semantic_layer/tests/test_malloy_xorq_roundtrip.py @@ -10,19 +10,11 @@ import pytest -from boring_semantic_layer.serialization import from_tagged, to_tagged, try_import_xorq +from boring_semantic_layer.serialization import from_tagged, to_tagged -# Check if xorq is available -try: - try_import_xorq() - xorq_available = True - xorq_skip_reason = "" -except ImportError: - xorq_available = False - xorq_skip_reason = "xorq not installed" +pytest.importorskip("xorq", reason="xorq not installed") -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestMalloyModelsRoundTrip: """Test round-trip conversion for all Malloy-inspired BSL models.""" @@ -385,7 +377,6 @@ def test_multiple_measures_roundtrip(self): assert len(reconstructed_data) == 3 -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestMalloyXorqFeatures: """Test that xorq-specific features work with Malloy-style BSL models.""" diff --git a/src/boring_semantic_layer/tests/test_measure_reference_styles.py b/src/boring_semantic_layer/tests/test_measure_reference_styles.py index fd802073..9a37329c 100644 --- a/src/boring_semantic_layer/tests/test_measure_reference_styles.py +++ b/src/boring_semantic_layer/tests/test_measure_reference_styles.py @@ -533,6 +533,7 @@ def test_method_call_serialization_roundtrip(): Replaces the legacy curated-AST direct construction with the behavioral round-trip through ``to_tagged`` / ``from_tagged``. """ + pytest.importorskip("xorq") from boring_semantic_layer import to_semantic_table from boring_semantic_layer.serialization import from_tagged, to_tagged diff --git a/src/boring_semantic_layer/tests/test_mutate_compositions.py b/src/boring_semantic_layer/tests/test_mutate_compositions.py index b139d43c..9cc2de4a 100644 --- a/src/boring_semantic_layer/tests/test_mutate_compositions.py +++ b/src/boring_semantic_layer/tests/test_mutate_compositions.py @@ -436,6 +436,7 @@ def test_legacy_semantic_mutate_tag_is_rejected(self): ) def test_mutated_aggregate_roundtrips_through_tagged(self, flights_model): + pytest.importorskip("xorq", reason="xorq not installed") expr = ( flights_model.group_by("carrier") .aggregate("flight_count", "total_distance") diff --git a/src/boring_semantic_layer/tests/test_real_world_scenarios.py b/src/boring_semantic_layer/tests/test_real_world_scenarios.py index 3d86407e..0403ed14 100644 --- a/src/boring_semantic_layer/tests/test_real_world_scenarios.py +++ b/src/boring_semantic_layer/tests/test_real_world_scenarios.py @@ -11,7 +11,9 @@ import ibis import pandas as pd import pytest -import xorq.api as xo + +xorq = pytest.importorskip("xorq", reason="xorq not installed") +import xorq.api as xo # noqa: E402 from boring_semantic_layer import to_semantic_table diff --git a/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown.py b/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown.py index 7e45b641..6fda2ac0 100644 --- a/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown.py +++ b/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown.py @@ -14,9 +14,12 @@ from boring_semantic_layer import to_semantic_table, to_untagged -# Projection pushdown disabled for xorq compatibility +# Projection pushdown is disabled in BSL (for xorq-vendored ibis compatibility), +# so these tests xfail regardless of whether xorq is installed. A few incidentally +# xpass under plain ibis where the emitted SQL happens to match; that is not the +# feature working, so the marker is intentionally unconditional and non-strict. pytestmark = pytest.mark.xfail( - reason="Projection pushdown disabled for xorq vendored ibis compatibility" + reason="Projection pushdown disabled for xorq-vendored ibis compatibility" ) diff --git a/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown_examples.py b/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown_examples.py index bc87e881..10cb23a8 100644 --- a/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown_examples.py +++ b/src/boring_semantic_layer/tests/test_rewrites_projection_pushdown_examples.py @@ -20,9 +20,12 @@ from boring_semantic_layer.api import to_semantic_table from boring_semantic_layer.expr import to_untagged -# Projection pushdown disabled for xorq compatibility +# Projection pushdown is disabled in BSL (for xorq-vendored ibis compatibility), +# so these tests xfail regardless of whether xorq is installed. A few incidentally +# xpass under plain ibis where the emitted SQL happens to match; that is not the +# feature working, so the marker is intentionally unconditional and non-strict. pytestmark = pytest.mark.xfail( - reason="Projection pushdown disabled for xorq vendored ibis compatibility" + reason="Projection pushdown disabled for xorq-vendored ibis compatibility" ) diff --git a/src/boring_semantic_layer/tests/test_upstream_ibis_pins.py b/src/boring_semantic_layer/tests/test_upstream_ibis_pins.py index 9ef2321a..7ce92982 100644 --- a/src/boring_semantic_layer/tests/test_upstream_ibis_pins.py +++ b/src/boring_semantic_layer/tests/test_upstream_ibis_pins.py @@ -22,7 +22,9 @@ import pytest import ibis as plain_ibis -from xorq.common.utils.ibis_utils import from_ibis + +pytest.importorskip("xorq", reason="xorq not installed") +from xorq.common.utils.ibis_utils import from_ibis # noqa: E402 def _make_table(flavor: str, name: str, df: pd.DataFrame): diff --git a/src/boring_semantic_layer/tests/test_xorq_backends.py b/src/boring_semantic_layer/tests/test_xorq_backends.py index 642b7788..93c09dc3 100644 --- a/src/boring_semantic_layer/tests/test_xorq_backends.py +++ b/src/boring_semantic_layer/tests/test_xorq_backends.py @@ -14,21 +14,11 @@ import pytest from boring_semantic_layer import SemanticModel -from boring_semantic_layer.serialization import from_tagged, to_tagged, try_import_xorq +from boring_semantic_layer.serialization import from_tagged, to_tagged -# Check if xorq is available -try: - try_import_xorq() - import xorq.api as xo +xo = pytest.importorskip("xorq.api", reason="xorq not installed") - xorq_available = True - xorq_skip_reason = "" -except ImportError: - xorq_available = False - xorq_skip_reason = "xorq not installed" - -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqDuckDBBackend: """Test BSL with xorq's DuckDB backend.""" @@ -123,7 +113,6 @@ def test_duckdb_with_pyarrow_batches(self): assert all_data == list(range(100)) -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqDataFusionBackend: """Test BSL with xorq's DataFusion backend.""" @@ -162,7 +151,6 @@ def test_datafusion_aggregation(self): assert set(df["grp"]) == {"A", "B"} -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqPandasBackend: """Test BSL with xorq's Pandas backend.""" @@ -192,7 +180,6 @@ def test_pandas_backend_execution(self): assert list(df["y"]) == [4, 5, 6] -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqBackendSwitching: """Test switching between different xorq backends.""" @@ -223,7 +210,6 @@ def test_multiple_backends_isolation(self): assert backend2 is not None -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqBackendFeatures: """Test xorq-specific backend features with BSL.""" diff --git a/src/boring_semantic_layer/tests/test_xorq_integration.py b/src/boring_semantic_layer/tests/test_xorq_integration.py index 952fa126..28c02c16 100644 --- a/src/boring_semantic_layer/tests/test_xorq_integration.py +++ b/src/boring_semantic_layer/tests/test_xorq_integration.py @@ -8,19 +8,11 @@ import pytest from boring_semantic_layer import SemanticModel -from boring_semantic_layer.serialization import from_tagged, to_tagged, try_import_xorq +from boring_semantic_layer.serialization import from_tagged, to_tagged -# Check if xorq is available -try: - try_import_xorq() - xorq_available = True - xorq_skip_reason = "" -except ImportError: - xorq_available = False - xorq_skip_reason = "xorq not installed" +pytest.importorskip("xorq", reason="xorq not installed") -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqIntegration: """Integration tests for xorq conversion.""" @@ -204,7 +196,6 @@ def test_filtered_expression_to_xorq(self): assert min(df["a"]) > 2 -@pytest.mark.skipif(not xorq_available, reason=xorq_skip_reason) class TestXorqFeatures: """Test xorq-specific features that aren't available in regular ibis.""" diff --git a/src/boring_semantic_layer/tests/test_xorq_rebuild.py b/src/boring_semantic_layer/tests/test_xorq_rebuild.py index a32431d7..443a21e1 100644 --- a/src/boring_semantic_layer/tests/test_xorq_rebuild.py +++ b/src/boring_semantic_layer/tests/test_xorq_rebuild.py @@ -16,15 +16,15 @@ import ibis import pytest -from boring_semantic_layer import SemanticModel -from boring_semantic_layer.serialization import to_tagged -from boring_semantic_layer.serialization.tag_handler import ( +xorq = pytest.importorskip("xorq", reason="xorq not installed") + +from boring_semantic_layer import SemanticModel # noqa: E402 +from boring_semantic_layer.serialization import to_tagged # noqa: E402 +from boring_semantic_layer.serialization.tag_handler import ( # noqa: E402 bsl_tag_handler, reemit, ) -xorq = pytest.importorskip("xorq", reason="xorq not installed") - from xorq.expr.builders import TagHandler as _TagHandler _has_reemit = "reemit" in {a.name for a in _TagHandler.__attrs_attrs__} diff --git a/src/boring_semantic_layer/tests/test_xorq_string_serialization.py b/src/boring_semantic_layer/tests/test_xorq_string_serialization.py index 337a1219..efe9f40f 100644 --- a/src/boring_semantic_layer/tests/test_xorq_string_serialization.py +++ b/src/boring_semantic_layer/tests/test_xorq_string_serialization.py @@ -3,6 +3,8 @@ import ibis import pytest +pytest.importorskip("xorq", reason="xorq not installed") + from boring_semantic_layer import to_semantic_table diff --git a/src/boring_semantic_layer/tests/test_xorq_tag_handler.py b/src/boring_semantic_layer/tests/test_xorq_tag_handler.py index a99ba611..34d56f41 100644 --- a/src/boring_semantic_layer/tests/test_xorq_tag_handler.py +++ b/src/boring_semantic_layer/tests/test_xorq_tag_handler.py @@ -17,6 +17,8 @@ import ibis import pytest +pytest.importorskip("xorq", reason="xorq not installed") + from boring_semantic_layer import SemanticModel, to_semantic_table from boring_semantic_layer.serialization import to_tagged from boring_semantic_layer.serialization.tag_handler import ( diff --git a/src/boring_semantic_layer/tests/test_yaml.py b/src/boring_semantic_layer/tests/test_yaml.py index c5cb3bf2..b3c08835 100644 --- a/src/boring_semantic_layer/tests/test_yaml.py +++ b/src/boring_semantic_layer/tests/test_yaml.py @@ -1436,6 +1436,53 @@ def test_yaml_calculated_measures_simple_format(duckdb_conn): assert "ratio" in df.columns +def test_yaml_calculated_measures_prefer_same_named_measures(duckdb_conn): + """YAML calculated measures resolve colliding names to measures, not raw columns.""" + tbl = duckdb_conn.create_table( + "calc_meas_collision_tbl", + { + "phase": ["approved", "approved", "review"], + "required_people": [10, 20, 5], + "scheduled_people": [12, 18, 10], + "target_cells": [2, 3, 1], + }, + ) + + config = { + "targets": { + "table": "calc_meas_collision_tbl", + "dimensions": {"phase": "_.phase"}, + "measures": { + "target_cells": "_.target_cells.sum()", + "required_people": "_.required_people.sum()", + "scheduled_people": "_.scheduled_people.sum()", + }, + "calculated_measures": { + "coverage_ratio": "_.scheduled_people / _.required_people", + "avg_required_per_cell": "_.required_people / _.target_cells", + }, + }, + } + + model = from_config(config, tables={"calc_meas_collision_tbl": tbl})["targets"] + assert set(model.get_calculated_measures()) == { + "coverage_ratio", + "avg_required_per_cell", + } + + df = ( + model.query( + dimensions=("phase",), + measures=("coverage_ratio", "avg_required_per_cell"), + ) + .execute() + .sort_values("phase") + .reset_index(drop=True) + ) + assert df["coverage_ratio"].tolist() == pytest.approx([1.0, 2.0]) + assert df["avg_required_per_cell"].tolist() == pytest.approx([6.0, 5.0]) + + # --------------------------------------------------------------------------- # Issue #115: .all() for window aggregations in YAML # --------------------------------------------------------------------------- diff --git a/src/boring_semantic_layer/yaml.py b/src/boring_semantic_layer/yaml.py index 830b86c6..2d416302 100644 --- a/src/boring_semantic_layer/yaml.py +++ b/src/boring_semantic_layer/yaml.py @@ -84,6 +84,12 @@ def _parse_calc_measure(name: str, config: str | dict) -> Measure: def _make_calc_fn(source: str): def calc_fn(scope): return safe_eval(source, context={"_": scope}).unwrap() + + # YAML ``calculated_measures`` are documented as expressions over + # measures, not raw columns. Prefer known measure names during calc + # classification so a measure named like its source column (the common + # ``revenue: _.revenue.sum()`` shape) resolves to the aggregate output. + calc_fn.__bsl_prefer_known__ = True return calc_fn measure_kwargs = {"metadata": extra_kwargs["metadata"]} if "metadata" in extra_kwargs else {} diff --git a/uv.lock b/uv.lock index 613d83f7..dcc4aec3 100644 --- a/uv.lock +++ b/uv.lock @@ -161,18 +161,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/f6/073d19f7b571c08327fbba3f8e011578da67ab62a11f98911274ff80653f/beartype-0.22.5-py3-none-any.whl", hash = "sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0", size = 1321700, upload-time = "2025-11-01T05:49:18.436Z" }, ] -[[package]] -name = "beniget" -version = "0.4.2.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gast" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/27/5bb01af8f2860d431b98d0721b96ff2cea979106cae3f2d093ec74f6400c/beniget-0.4.2.post1.tar.gz", hash = "sha256:a0258537e65e7e14ec33a86802f865a667f949bb6c73646d55e42f7c45a052ae", size = 32274, upload-time = "2024-06-28T10:20:05.708Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/e4/6e8731d4d10dd09942a6f5015b2148ae612bf13e49629f33f9fade3c8253/beniget-0.4.2.post1-py3-none-any.whl", hash = "sha256:e1b336e7b5f2ae201e6cc21f533486669f1b9eccba018dcff5969cd52f1c20ba", size = 17242, upload-time = "2024-06-28T10:20:03.197Z" }, -] - [[package]] name = "boring-semantic-layer" version = "0.3.14" @@ -184,7 +172,6 @@ dependencies = [ { name = "pyyaml" }, { name = "returns" }, { name = "toolz" }, - { name = "xorq" }, ] [package.optional-dependencies] @@ -195,31 +182,16 @@ agent = [ { name = "rich" }, ] dev = [ - { name = "altair" }, - { name = "duckdb" }, - { name = "fastapi" }, - { name = "fastmcp" }, - { name = "hypothesis" }, - { name = "kaleido" }, - { name = "langchain" }, { name = "langchain-anthropic" }, { name = "langchain-openai" }, { name = "malloy" }, - { name = "nbformat" }, { name = "openai" }, { name = "pandas" }, - { name = "plotext" }, - { name = "plotly" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "python-dotenv" }, - { name = "rich" }, { name = "ruff" }, { name = "urllib3" }, - { name = "uvicorn", extra = ["standard"] }, - { name = "vl-convert-python" }, - { name = "xorq", extra = ["duckdb"] }, ] examples = [ { name = "duckdb" }, @@ -232,6 +204,12 @@ server = [ { name = "fastapi" }, { name = "uvicorn", extra = ["standard"] }, ] +test-core = [ + { name = "ibis-framework", extra = ["duckdb"] }, + { name = "pandas" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] viz-altair = [ { name = "altair" }, { name = "vl-convert-python" }, @@ -244,6 +222,9 @@ viz-plotly = [ { name = "nbformat" }, { name = "plotly" }, ] +xorq = [ + { name = "xorq" }, +] [package.dev-dependencies] dev = [ @@ -255,12 +236,11 @@ dev = [ requires-dist = [ { name = "altair", marker = "extra == 'viz-altair'", specifier = ">=5.0.0" }, { name = "attrs", specifier = ">=25.3.0" }, - { name = "boring-semantic-layer", extras = ["agent", "mcp", "server", "examples", "viz-altair", "viz-plotly", "viz-plotext"], marker = "extra == 'dev'" }, { name = "duckdb", marker = "extra == 'examples'", specifier = "<1.4" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115.0" }, { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.12.4" }, - { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "ibis-framework", specifier = ">=11.0.0" }, + { name = "ibis-framework", extras = ["duckdb"], marker = "extra == 'test-core'", specifier = ">=11.0.0" }, { name = "kaleido", marker = "extra == 'viz-plotly'" }, { name = "langchain", marker = "extra == 'agent'", specifier = ">=0.3.0" }, { name = "langchain-anthropic", marker = "extra == 'dev'", specifier = ">=0.3.0" }, @@ -270,12 +250,15 @@ requires-dist = [ { name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "packaging" }, { name = "pandas", marker = "extra == 'dev'", specifier = ">=2.3.0" }, + { name = "pandas", marker = "extra == 'test-core'", specifier = ">=2.3.0" }, { name = "plotext", marker = "extra == 'agent'", specifier = ">=5.0.0" }, { name = "plotext", marker = "extra == 'viz-plotext'", specifier = ">=5.0.0" }, { name = "plotly", marker = "extra == 'viz-plotly'", specifier = ">=6.3.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test-core'" }, { name = "pytest-asyncio", marker = "extra == 'dev'" }, + { name = "pytest-asyncio", marker = "extra == 'test-core'" }, { name = "python-dotenv", marker = "extra == 'agent'", specifier = ">=1.0.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "returns", specifier = ">=0.26.0" }, @@ -285,11 +268,10 @@ requires-dist = [ { name = "urllib3", marker = "extra == 'dev'", specifier = ">=2.2.3" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.30.0" }, { name = "vl-convert-python", marker = "extra == 'viz-altair'", specifier = ">=1.0.0" }, - { name = "xorq", specifier = ">=0.3.25" }, - { name = "xorq", marker = "extra == 'examples'" }, - { name = "xorq", extras = ["duckdb"], marker = "extra == 'examples'", specifier = ">=0.3.4" }, + { name = "xorq", marker = "extra == 'xorq'", specifier = ">=0.3.31" }, + { name = "xorq", extras = ["duckdb"], marker = "extra == 'examples'", specifier = ">=0.3.31" }, ] -provides-extras = ["examples", "viz-altair", "viz-plotly", "viz-plotext", "agent", "mcp", "server", "dev"] +provides-extras = ["xorq", "examples", "viz-altair", "viz-plotly", "viz-plotext", "agent", "mcp", "server", "test-core", "dev"] [package.metadata.requires-dev] dev = [{ name = "ipython", specifier = ">=8.38.0" }] @@ -533,38 +515,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/b9/c3693aa56c0da6acd66b553837b16a770b4feaf0df9bafc203ab2e42eeb9/choreographer-1.2.0-py3-none-any.whl", hash = "sha256:00892baf912fc08b169488a56a9000d61c221d7a024eb4726dc623bc2e2f1b07", size = 56361, upload-time = "2025-10-23T00:32:55.694Z" }, ] -[[package]] -name = "cityhash" -version = "0.4.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/29/06f572448a407cbcc6565b4568086da70552a74a850ef20c9c73f1cbbf81/cityhash-0.4.10.tar.gz", hash = "sha256:7e35da9aaf5fcf91da3fea23405874db55ffa58b1abc441d39cce0c8704a9c15", size = 274911, upload-time = "2025-10-09T21:57:51.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/fc/690b4f0b54ce19c451c35e4fd1efa5a6eab268d72f37b6d322e22dede9b5/cityhash-0.4.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:75f3d40a634cad68b32323528b57b2f43f5abb6f687fd504de77eaba9c29621c", size = 73721, upload-time = "2025-10-09T21:57:12.031Z" }, - { url = "https://files.pythonhosted.org/packages/99/58/f28ba8ef0041fcddb630228907e42cbe1ae66f716d1b9252c63bbfa113fc/cityhash-0.4.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9293b4c79874c7c02d9201a9ee28b6b400751ce6517662cf1bc0a8d42c1c150", size = 67690, upload-time = "2025-10-09T21:57:13.195Z" }, - { url = "https://files.pythonhosted.org/packages/17/b9/4a5fdb49b275f3695df4c779342dc80814a3d18ba752de28950ac6fe2ce9/cityhash-0.4.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce38b677f01cea7979cc3c98c4fdc6bec535b32a4005cdc5506c1f9d656ff87c", size = 352181, upload-time = "2025-10-09T21:57:14.111Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/659ce64854a2b4b32204840bc71a8e70408deb95068bca49175a71a6f3be/cityhash-0.4.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390b81885de9dd5a2ec247deb7b8f5dcc74ea11c3b50222c914f99c4c6961fcd", size = 559055, upload-time = "2025-10-09T21:57:15.085Z" }, - { url = "https://files.pythonhosted.org/packages/fd/49/20e30aa0a591256ccf61bd5ba4ad5117ab6f44cac3d0aa126586b683011c/cityhash-0.4.10-cp310-cp310-win32.whl", hash = "sha256:8355ab427a364e30cbe73296fe3b169154023258055e53c60dd16daa85211e6d", size = 63098, upload-time = "2025-10-09T21:57:16.029Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0c/7dc604c92648930aa7fce0eb4a5903da0600ff76308bdc2ffcfe6c77275f/cityhash-0.4.10-cp310-cp310-win_amd64.whl", hash = "sha256:a5f1451022550f5b5d87a62aced1bf9218e8cab9af14b9d1980a2e01acf281ee", size = 58115, upload-time = "2025-10-09T21:57:16.77Z" }, - { url = "https://files.pythonhosted.org/packages/bf/50/5406dfafda0e3b0f9e3ae64cf5441c7b884e4f95b148d7d4a60b6568d769/cityhash-0.4.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86ccc9893733dae9868c5d279b9b61518f55034127ca0f1ed51946fe6578a33d", size = 73056, upload-time = "2025-10-09T21:57:17.917Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/a0bdff0e2d18be6c77c4d98549c9dd5479bc38db224626fa6953cd1bee19/cityhash-0.4.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2eb819ba8fb6f6615ef5b8e9200060a7a66a07f14ef55bd29c0916ada55bad4", size = 67147, upload-time = "2025-10-09T21:57:18.957Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f0/295fa70bc1e18d087d01151489baae1eb679b776e8671ed80f300e1ba8a1/cityhash-0.4.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8911ff08138dc6f2134c9a1c19c7e1972a2f198b210c19f2123c999c34d37ca", size = 371848, upload-time = "2025-10-09T21:57:19.705Z" }, - { url = "https://files.pythonhosted.org/packages/22/f2/1d1a2134c6ec94f3871c8c41ebbd893dfb8f20596a93addbfd534243fc8f/cityhash-0.4.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96043b803797fe8c8c632edb708ed5a22c72b38c1539b33079be1ec45323290e", size = 586426, upload-time = "2025-10-09T21:57:20.667Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fe/81028167c4cfc96b1d1aa807ec542574dfc6bfe0d28b04cd285dd21b4798/cityhash-0.4.10-cp311-cp311-win32.whl", hash = "sha256:61ec6cf9b0d9895eefb57e1b5151f8731a0e7294ed400741b2a2d596dc21f09d", size = 62879, upload-time = "2025-10-09T21:57:21.778Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6b/10a559d1c89f304a8258a1bd93d7b2c4ad512e4b05b6304e37f73fd7f96d/cityhash-0.4.10-cp311-cp311-win_amd64.whl", hash = "sha256:c04e42ecb23b9c0e7ee4cade50ad9908671f594dfa5586d7838beca9b42adc51", size = 58483, upload-time = "2025-10-09T21:57:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c0/90bc87f5bfe9a9f05e6a14c90421dcc1f03e1c230d14fb41e668979a464c/cityhash-0.4.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:65b5739609833155c59152de0496a8edfd964d89cf9fafbd3b18e75b79acd84f", size = 75149, upload-time = "2025-10-09T21:57:23.536Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e7/ad02bf676baeb3f6214647b0041a406a1c23355a004f7aa1d5546f612957/cityhash-0.4.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a507f8c0633ae65edacc7f43340c413b99e32f2274420058e1e0f7887108bfea", size = 68495, upload-time = "2025-10-09T21:57:24.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/66/32faa612b2056ba9c0201a43abdb1c47b342c2233305646a1ad2941e849d/cityhash-0.4.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d72afa1b5b7b682d40a5e6d41b2837c0a6f32ac81860be52d54bbf7044520d2", size = 380917, upload-time = "2025-10-09T21:57:25.387Z" }, - { url = "https://files.pythonhosted.org/packages/40/d1/283aef1770b212d4079308c854ca859d5e2e56ead875d9f181edeec2c4ea/cityhash-0.4.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aeb6a3f455e73765d0f1844b5a5004ab89f531a99b82533cfdaa0bc9f616544", size = 599390, upload-time = "2025-10-09T21:57:26.254Z" }, - { url = "https://files.pythonhosted.org/packages/88/94/77bab6a5e9727b56353f25c52ec450918dbe3bb0b4613619f2ff8c3d17f3/cityhash-0.4.10-cp312-cp312-win32.whl", hash = "sha256:e4b92bec4001d4fd72e145c56696d751bb011dd122443a1207713eb811d4e0ae", size = 63778, upload-time = "2025-10-09T21:57:27.142Z" }, - { url = "https://files.pythonhosted.org/packages/5b/84/eb797effbf96c02dae5c8962f07a713d348d6807ee6ae379afef0609695d/cityhash-0.4.10-cp312-cp312-win_amd64.whl", hash = "sha256:bc0a0aed8d1c1aa0b21500d42995ed0a6eb1785746529066d2bd5d7abfee91ba", size = 58886, upload-time = "2025-10-09T21:57:27.885Z" }, - { url = "https://files.pythonhosted.org/packages/ac/20/6ef589058097c634b719b324aed5fcda8a0798ea01640560e13142c1f27b/cityhash-0.4.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c04b3bcebcada20c650ec0a5dc1c2933170c740b439595924791fa7c167107", size = 73344, upload-time = "2025-10-09T21:57:28.653Z" }, - { url = "https://files.pythonhosted.org/packages/e4/78/56c76732199800ffbc400df12667bd8c9f24d9e5f7b5b2e3feec06aba4f2/cityhash-0.4.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38577d4b1965edfcc656f09813a3554572235f5e31de198cd44e6bc1f96323a2", size = 66853, upload-time = "2025-10-09T21:57:29.416Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f1/3bdf7b449740c0eee7a8c593e1ac11a3b6154602e2c3de63ba8d3e38d34a/cityhash-0.4.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1edcae23ebe925a4b309a6be4bf34c03577a6ef73a52c027e17ce324dd84745", size = 366702, upload-time = "2025-10-09T21:57:30.197Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bd/a17d4ae773a4dce0185937b92b29da4ce584839cb69d9036d23505957dc4/cityhash-0.4.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584adb2d108453eb486a252113addb5239c570d13b81302f198b94cdb5e16b36", size = 585208, upload-time = "2025-10-09T21:57:31.156Z" }, - { url = "https://files.pythonhosted.org/packages/d5/83/e750a34a43c59f51afc77de7554b5b3e77bec632a0e63090647798b9015c/cityhash-0.4.10-cp313-cp313-win32.whl", hash = "sha256:c5f14232a7ab12cb173ebe885c1a967a0e9a7ead7695bba7af58c6d2aea13970", size = 62818, upload-time = "2025-10-09T21:57:32.014Z" }, - { url = "https://files.pythonhosted.org/packages/c0/36/ba93514a5888b8139ee5ff4efef47cbf086d3433ce739f2d252faf5890d5/cityhash-0.4.10-cp313-cp313-win_amd64.whl", hash = "sha256:4182c95c2615a3f8d5ffd06c7204a1d2f9296f0f6abd4f34f8a6d6d199ed7a3f", size = 57387, upload-time = "2025-10-09T21:57:32.749Z" }, -] - [[package]] name = "click" version = "8.3.0" @@ -677,25 +627,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/1d/2b313e157c9c7bba319e42f464d15073d32a81ac4827bdc5b7de38832b3e/cyclopts-4.2.1-py3-none-any.whl", hash = "sha256:17a801faa814988b0307385ef8aaeb6b14b4d64473015a2d66bde9ea13f14d9c", size = 184333, upload-time = "2025-10-31T14:30:57.581Z" }, ] -[[package]] -name = "dask" -version = "2025.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "cloudpickle" }, - { name = "fsspec" }, - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, - { name = "packaging" }, - { name = "partd" }, - { name = "pyyaml" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/41/43eb54e0f6d1ba971d5adcad8f0862b327af6a2041aa134acbcec630ad43/dask-2025.1.0.tar.gz", hash = "sha256:bb807586ff20f0f59f3d36fe34eb4a95f75a1aae2a775b521de6dd53727d2063", size = 10758681, upload-time = "2025-01-17T16:54:13.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a0/016d956a3fec193e3a5b466ca912944669c18dccc736b64a9e28ccdcc5f7/dask-2025.1.0-py3-none-any.whl", hash = "sha256:db86220c8d19bdf464cbe11a87a2c8f5d537acf586bb02eed6d61a302af5c2fd", size = 1371235, upload-time = "2025-01-17T16:54:09.918Z" }, -] - [[package]] name = "decorator" version = "5.2.1" @@ -808,18 +739,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "envyaml" -version = "1.10.211231" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/ce/bcc062f1d55368713674cf55851a4d9dfa77835c0258753d0f23dff70743/envyaml-1.10.211231.tar.gz", hash = "sha256:88f8a076159e3c317d3450a5f404132b6ac91aecee4934ea72eac65f911f1244", size = 7591, upload-time = "2022-01-08T10:56:40.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/99/6612dcf7d494223041c029cc4fa325cb513fe99bf989e6895a1de357f1eb/envyaml-1.10.211231-py2.py3-none-any.whl", hash = "sha256:8d7a7a6be12587cc5da32a587067506b47b849f4643981099ad148015a72de52", size = 8138, upload-time = "2022-01-08T10:56:38.849Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.0" @@ -900,45 +819,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] -[[package]] -name = "fsspec" -version = "2025.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, -] - -[[package]] -name = "gast" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/14/c566f5ca00c115db7725263408ff952b8ae6d6a4e792ef9c84e77d9af7a1/gast-0.6.0.tar.gz", hash = "sha256:88fc5300d32c7ac6ca7b515310862f71e6fdf2c029bbec7c66c0f5dd47b6b1fb", size = 27708, upload-time = "2024-06-27T20:31:49.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/61/8001b38461d751cd1a0c3a6ae84346796a5758123f3ed97a1b121dfbf4f3/gast-0.6.0-py3-none-any.whl", hash = "sha256:52b182313f7330389f72b069ba00f174cfe2a06411099547288839c6cbafbd54", size = 21173, upload-time = "2024-07-09T13:15:15.615Z" }, -] - -[[package]] -name = "geoarrow-types" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/97/fa35f5d13a803b8f16e59c1f18f06607b9df5683c08bd7cd7a48a29ce988/geoarrow_types-0.3.0.tar.gz", hash = "sha256:82243e4be88b268fa978ae5bba6c6680c3556735e795965b2fe3e6fbfea9f9ee", size = 23708, upload-time = "2025-05-27T03:39:39.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/16/e37cb1b0894c9cf3f9b1c50ebcfab56a0d9fe7c3b6f97d5680a7eb27ca08/geoarrow_types-0.3.0-py3-none-any.whl", hash = "sha256:439df6101632080442beccc7393cac54d6c7f6965da897554349e94d2492f613", size = 19025, upload-time = "2025-05-27T03:39:38.652Z" }, -] - -[[package]] -name = "git-annex" -version = "10.20260316" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/09/8a6fe029eae7c86ef36e123aa01e849245feac95d5088a34d7136fee8d9c/git_annex-10.20260316-py3-none-macosx_14_0_arm64.whl", hash = "sha256:171e0e096b7551cbedf7dc04eb5254721dbcdf9d18706fc7e926443afba777dc", size = 36279485, upload-time = "2026-03-17T17:33:31.127Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a4/f69d1f801d8d5a8fc03392718f737cfe1850706ad053eb06234ddf3cfd7a/git_annex-10.20260316-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:21fc91c6fb31abe38890d774ed5b39436cfd3fe589e38772be519f5e633d71b8", size = 14231272, upload-time = "2026-03-17T17:51:37.648Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/04428018a0716a95f714fc0ce1a081e56c7796273ca3a959638d5b01643a/git_annex-10.20260316-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:9ddc96a077283df17c02b287fc07874546fc9cd32dc3904d996bc841ac04cdf7", size = 26247156, upload-time = "2026-03-17T17:24:28.34Z" }, - { url = "https://files.pythonhosted.org/packages/0f/36/b5f70dade258ce23ac0a7b33e4c8184be3dadb5a9fbad0bb773927ae1248/git_annex-10.20260316-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:157ec16270aa3cd6fd45f16de8963d9f9c298c260ec0fd460c92a2a52ea5a789", size = 21888939, upload-time = "2026-03-17T17:27:02.262Z" }, - { url = "https://files.pythonhosted.org/packages/a7/12/9689ee50c88d5632e612703505b104ae41a3f2d8b9a1fd27cb2f4744c3ae/git_annex-10.20260316-py3-none-win_amd64.whl", hash = "sha256:8bc63486ee1beffd7200f4e7e3323123641dd146067b104547bd85209b00b9b6", size = 27056643, upload-time = "2026-03-17T17:31:17.57Z" }, -] - [[package]] name = "gitdb" version = "4.0.12" @@ -1253,19 +1133,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, ] -[[package]] -name = "hypothesis" -version = "6.155.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/04/64032a1dccd2233615c8a3f701bbb563558575ed017496a24b6d81762c91/hypothesis-6.155.2.tar.gz", hash = "sha256:ae36880287c9c5defe9f199d3d2b67d9947a4da2a46e6c57373cbdf2345b20e1", size = 477765, upload-time = "2026-06-05T16:32:23.63Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6e/e735f27ac1a530a4cd0a31cd970ec495a3a11830fdc5d281cc292593b330/hypothesis-6.155.2-py3-none-any.whl", hash = "sha256:c85ce6dcd630a90ce501f1d1dd1bc84b97f5649ca8a27e134c8cbf5aa480b1a5", size = 544213, upload-time = "2026-06-05T16:32:21.15Z" }, -] - [[package]] name = "ibis-framework" version = "11.0.0" @@ -1284,6 +1151,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/c0/2851a8a55d0fea03b80fd45815069b686e032938fc68fa9d91ac776c148c/ibis_framework-11.0.0-py3-none-any.whl", hash = "sha256:92ff82a96f4eac7f86fa9b6a315e04b5a8f9ed3d186539d88f48e628363f2e72", size = 1935652, upload-time = "2025-10-15T13:12:07.954Z" }, ] +[package.optional-dependencies] +duckdb = [ + { name = "duckdb" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyarrow-hotfix" }, + { name = "rich" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -1819,15 +1698,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] -[[package]] -name = "locket" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, -] - [[package]] name = "logistro" version = "2.0.1" @@ -2587,19 +2457,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/fc/8cb9073bb1bee54eb49a1ae501a36402d01763812962ac811cdc1c81a9d7/parsy-2.2-py3-none-any.whl", hash = "sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7", size = 9538, upload-time = "2025-09-12T11:39:25.749Z" }, ] -[[package]] -name = "partd" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "locket" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, -] - [[package]] name = "pathable" version = "0.4.4" @@ -2670,15 +2527,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "ply" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, -] - [[package]] name = "pre-commit" version = "4.3.0" @@ -3173,23 +3021,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] -[[package]] -name = "pythran" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beniget" }, - { name = "gast" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "ply" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/0a/95a72f09f25dae48f41e367959075ed4c7a0ff02dd3f54eec111501d648a/pythran-0.18.0.tar.gz", hash = "sha256:5c003e8cbedf6dbb68c2869c49fc110ce8b5e8982993078a4a819f1dadc4fc6a", size = 2402895, upload-time = "2025-05-23T09:30:57.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/49/c5c72ebb49edf56bb06d3b805870cf6598565461670d88d292085ac96bfe/pythran-0.18.0-py3-none-any.whl", hash = "sha256:405ecf2100d4926d1a15640c36bd1b19a560386653d0ee4d5234f9421ef4034b", size = 4343521, upload-time = "2025-05-23T09:30:54.741Z" }, -] - [[package]] name = "pytz" version = "2025.2" @@ -3665,15 +3496,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - [[package]] name = "simplejson" version = "3.20.2" @@ -4406,20 +4228,15 @@ wheels = [ [[package]] name = "xorq" -version = "0.3.25" +version = "0.3.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "atpublic" }, { name = "attrs", marker = "python_full_version < '4'" }, - { name = "cityhash", marker = "python_full_version < '4'" }, { name = "click" }, { name = "cloudpickle" }, { name = "cryptography" }, - { name = "dask", marker = "python_full_version < '4'" }, - { name = "envyaml" }, { name = "filelock" }, - { name = "geoarrow-types", marker = "python_full_version < '4'" }, - { name = "git-annex" }, { name = "gitpython" }, { name = "opentelemetry-exporter-otlp" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, @@ -4432,7 +4249,6 @@ dependencies = [ { name = "pyarrow", marker = "python_full_version < '4'" }, { name = "pyarrow-hotfix", marker = "python_full_version < '4'" }, { name = "python-dateutil" }, - { name = "pythran", marker = "sys_platform == 'darwin'" }, { name = "pytz" }, { name = "rich" }, { name = "sqlglot" }, @@ -4443,11 +4259,12 @@ dependencies = [ { name = "toolz" }, { name = "typing-extensions" }, { name = "uv" }, + { name = "xorq-dasher" }, { name = "xorq-datafusion" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/49/260378a74cfc43858ded0af74887abe90ea5a38b1c27487febc4d82f402a/xorq-0.3.25.tar.gz", hash = "sha256:ad12f17ea6958cfee786cc18e6cdd5b9602c7d2cfcb763133302817f8adb5e18", size = 1779036, upload-time = "2026-05-19T12:00:38.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/c8/a36f8dd180d046dc0aebc841af65fceb4a368ca2615bb62ddb172fb5ff11/xorq-0.3.31.tar.gz", hash = "sha256:1db64c7679bfbfc15d4e8f6f42d13cbb2b9f909c3ffb352b0d3cde53281b29e3", size = 1888543, upload-time = "2026-06-24T18:30:43.325Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/20/378d9c10d5660718d36af863ec77f1f5ffb912e9565bb4adb89d27bdd039/xorq-0.3.25-py3-none-any.whl", hash = "sha256:8502c64f10559f8496af76f397a860e5c41bf0b5372de54ef7c241ccca696e06", size = 1705626, upload-time = "2026-05-19T12:00:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/7e9cc090c5b33a785e407f379b4e223b0dba8aebbdb32f1ab29963b48ebd/xorq-0.3.31-py3-none-any.whl", hash = "sha256:cf28e28a95d86e151aabcad9e0a739f1ccdb189143b2d4720643bb384ed154fd", size = 1812860, upload-time = "2026-06-24T18:30:41.666Z" }, ] [package.optional-dependencies] @@ -4455,6 +4272,20 @@ duckdb = [ { name = "duckdb" }, ] +[[package]] +name = "xorq-dasher" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "toolz" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/be/861c374cc117e77007d6159008c120f3505633620b5e931058a9eaea8acf/xorq_dasher-0.1.1.tar.gz", hash = "sha256:377a590f3c491e9b8dc2599e7869eb8f8c0a3d56e38965073afb284fa5942fef", size = 100088, upload-time = "2026-05-26T13:29:30.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/3c/6e87dc23b6458b83477afd8350e1119cf11b909054d5f04c8e4b867a79d6/xorq_dasher-0.1.1-py3-none-any.whl", hash = "sha256:acaf63a61dea249781107cd88b67d36d5a3f47505832a36919e13f00c51bf3fd", size = 9405, upload-time = "2026-05-26T13:29:29.092Z" }, +] + [[package]] name = "xorq-datafusion" version = "0.2.7"