diff --git a/src/boring_semantic_layer/_xorq.py b/src/boring_semantic_layer/_xorq.py new file mode 100644 index 00000000..167d714b --- /dev/null +++ b/src/boring_semantic_layer/_xorq.py @@ -0,0 +1,104 @@ +"""Single import point for the xorq surface used by BSL. + +All BSL modules should import xorq symbols from this shim. If xorq renames +or moves something, only this file needs to change. + +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. +""" + +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 + +__all__ = [ + "Attr", + "BinaryOperator", + "CachedNode", + "Call", + "Column", + "Config", + "DatabaseTable", + "Deferred", + "Expr", + "Field", + "FrozenDict", + "FrozenOrderedDict", + "Graph", + "GroupedTable", + "Item", + "JoinChain", + "JoinReference", + "Just", + "JustUnhashable", + "Literal", + "Mapping", + "Node", + "Profile", + "Read", + "RemoteTable", + "Schema", + "Sequence", + "SortKey", + "Table", + "Tag", + "TagHandler", + "UnaryOperator", + "Variable", + "_", + "api", + "fmt", + "from_ibis", + "ibis", + "map_ibis", + "operations", + "relations", + "render_fields", + "replace_nodes", + "selectors", + "to_node", + "types", + "walk_nodes", +] diff --git a/src/boring_semantic_layer/chart/md_parser/executor.py b/src/boring_semantic_layer/chart/md_parser/executor.py index 1bd61cfb..3f127148 100644 --- a/src/boring_semantic_layer/chart/md_parser/executor.py +++ b/src/boring_semantic_layer/chart/md_parser/executor.py @@ -5,10 +5,10 @@ from typing import Any import ibis -import xorq.api as xo from returns.result import Success from boring_semantic_layer import to_semantic_table +from boring_semantic_layer._xorq import api as xo from boring_semantic_layer.utils import safe_eval diff --git a/src/boring_semantic_layer/compile_all.py b/src/boring_semantic_layer/compile_all.py index 06998ddd..59154a1b 100644 --- a/src/boring_semantic_layer/compile_all.py +++ b/src/boring_semantic_layer/compile_all.py @@ -145,7 +145,7 @@ def _get_ibis_module(table): table_module = type(table).__module__ if table_module.startswith("xorq.vendor.ibis"): # Table is from xorq's vendored ibis - from xorq.vendor import ibis as xorq_ibis + from ._xorq import ibis as xorq_ibis return xorq_ibis else: # Table is from regular ibis diff --git a/src/boring_semantic_layer/config.py b/src/boring_semantic_layer/config.py index 18d088c2..8e578b47 100644 --- a/src/boring_semantic_layer/config.py +++ b/src/boring_semantic_layer/config.py @@ -9,7 +9,7 @@ to reduce data scanned, which is especially beneficial for wide tables. """ -from xorq.vendor.ibis.config import Config +from ._xorq import Config class Options(Config): diff --git a/src/boring_semantic_layer/expr.py b/src/boring_semantic_layer/expr.py index 91f3d9d7..db757667 100644 --- a/src/boring_semantic_layer/expr.py +++ b/src/boring_semantic_layer/expr.py @@ -12,9 +12,11 @@ from ibis.expr.types.groupby import GroupedTable as IbisGroupedTable from ibis.expr.types.relations import Table as IbisTable from returns.result import Success, safe -from xorq.vendor.ibis.expr.types import Table -from xorq.vendor.ibis.expr.types.generic import Column as XorqColumn -from xorq.vendor.ibis.expr.types.groupby import GroupedTable +from ._xorq import ( + Column as XorqColumn, + GroupedTable, + Table, +) from .chart import chart as create_chart from .measure_scope import AggregationExpr, MeasureScope @@ -1253,7 +1255,7 @@ def collect_struct(struct_dict): # each can only infer types from columns of its own module first_col = next(iter(struct_dict.values())) if isinstance(first_col, XorqColumn): - import xorq.vendor.ibis as xibis + from ._xorq import ibis as xibis return xibis.struct(struct_dict).collect() return ibis.struct(struct_dict).collect() diff --git a/src/boring_semantic_layer/format.py b/src/boring_semantic_layer/format.py index bac9536d..ac4999a6 100644 --- a/src/boring_semantic_layer/format.py +++ b/src/boring_semantic_layer/format.py @@ -5,7 +5,7 @@ try: from ibis.expr.format import fmt, render_fields except ImportError: - from xorq.vendor.ibis.expr.format import fmt, render_fields + from ._xorq import fmt, render_fields from boring_semantic_layer.ops import ( SemanticAggregateOp, diff --git a/src/boring_semantic_layer/graph_utils.py b/src/boring_semantic_layer/graph_utils.py index 3bdae9c6..424d6623 100644 --- a/src/boring_semantic_layer/graph_utils.py +++ b/src/boring_semantic_layer/graph_utils.py @@ -11,20 +11,18 @@ from returns.maybe import Maybe, Nothing, Some from returns.result import Failure, Result, Success, safe from toolz import compose -from xorq.common.utils.graph_utils import ( +from ._xorq import ( + Expr as XorqExpr, + Graph, + Node, replace_nodes as _xorq_replace_nodes, -) -from xorq.common.utils.graph_utils import ( to_node as _xorq_to_node, ) -from xorq.vendor.ibis.common.graph import Graph -from xorq.vendor.ibis.expr.operations.core import Node -from xorq.vendor.ibis.expr.types import Expr as XorqExpr def _collect_field_types() -> tuple[type, ...]: """Collect all Field types that may appear in expressions.""" - from xorq.vendor.ibis.expr.operations.relations import Field as XorqField + from ._xorq import Field as XorqField types = [XorqField] try: diff --git a/src/boring_semantic_layer/measure_scope.py b/src/boring_semantic_layer/measure_scope.py index 38ad485f..5a0874d5 100644 --- a/src/boring_semantic_layer/measure_scope.py +++ b/src/boring_semantic_layer/measure_scope.py @@ -373,7 +373,7 @@ def __getitem__(self, name: str): return _resolve_column_item(self.tbl, name) def all(self, ref): - from xorq.vendor import ibis as ibis_mod + from ._xorq import ibis as ibis_mod if isinstance(ref, str): if self.post_agg: @@ -431,7 +431,7 @@ def __getitem__(self, name: str): return self.tbl[name] def all(self, ref): - from xorq.vendor import ibis as ibis_mod + from ._xorq import ibis as ibis_mod if isinstance(ref, str): return self.tbl[ref].sum().over(ibis_mod.window()) diff --git a/src/boring_semantic_layer/nested_access.py b/src/boring_semantic_layer/nested_access.py index f81e568e..4706b2bb 100644 --- a/src/boring_semantic_layer/nested_access.py +++ b/src/boring_semantic_layer/nested_access.py @@ -21,7 +21,7 @@ from attrs import frozen from toolz import curry -from xorq.vendor.ibis.expr import types as ir +from ._xorq import types as ir @frozen diff --git a/src/boring_semantic_layer/ops.py b/src/boring_semantic_layer/ops.py index ff2f534b..a73189e9 100644 --- a/src/boring_semantic_layer/ops.py +++ b/src/boring_semantic_layer/ops.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any import ibis -from xorq.api import selectors as s from attrs import field, frozen from ibis.common.deferred import Deferred from ibis.expr import datatypes as dt @@ -17,9 +16,13 @@ from ibis.expr.operations.relations import Field, Relation from ibis.expr.schema import Schema -from xorq.vendor.ibis.common.collections import FrozenDict, FrozenOrderedDict -from xorq.vendor.ibis.expr import operations as xorq_ops -from xorq.vendor.ibis.expr.schema import Schema as XorqSchema +from ._xorq import ( + FrozenDict, + FrozenOrderedDict, + Schema as XorqSchema, + operations as xorq_ops, + selectors as s, +) _SchemaClass = XorqSchema _FrozenOrderedDict = FrozenOrderedDict @@ -204,8 +207,8 @@ def _patch_xorq_sortkey_compat(): while xorq's vendored ibis keeps ``SortKey.expr``. Handle both. """ from ibis.expr.operations.sortkeys import SortKey as IbisSortKey - from xorq.common.utils.ibis_utils import map_ibis - from xorq.vendor.ibis.expr.operations.sortkeys import SortKey as XorqSortKey + + from ._xorq import SortKey as XorqSortKey, map_ibis if IbisSortKey in map_ibis.registry: return # already patched @@ -236,7 +239,7 @@ def _ensure_xorq_table(table): _patch_xorq_sortkey_compat() if "xorq.vendor.ibis" not in type(table).__module__: try: - from xorq.common.utils.ibis_utils import from_ibis + from ._xorq import from_ibis return from_ibis(table) except Exception: @@ -252,7 +255,7 @@ def _rebind_to_backend(expr, target_backend): reason; callers must pass a xorq-vendored ``target_backend``. """ try: - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel except Exception: return expr @@ -285,8 +288,7 @@ def _rebind_to_canonical_backend(expr): No-op on plain ibis expressions (not xorq-vendored). """ try: - from xorq.common.utils.node_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel, walk_nodes except Exception: return expr @@ -471,7 +473,7 @@ def _resolve_expr(expr: Deferred | Callable | Any, scope: ir.Table) -> ir.Value: scope_is_xorq = "xorq.vendor.ibis" in scope_module if result_is_regular_ibis and scope_is_xorq: - from xorq.common.utils.ibis_utils import from_ibis + from ._xorq import from_ibis result = from_ibis(result) @@ -4062,8 +4064,7 @@ def _rebind_join_backends(left_tbl, right_tbl): returning the inputs unchanged so ibis executes the join natively. """ try: - from xorq.common.utils.node_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from ._xorq import relations as xorq_rel, walk_nodes except Exception: return left_tbl, right_tbl @@ -4247,7 +4248,7 @@ def _get_weight_expr( all_roots: list, is_string: bool, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo if not by_measure: return xo._.count() @@ -4266,7 +4267,7 @@ def _build_string_index_fragment( type_str: str, weight_expr: Any, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo return ( base_tbl.group_by(field_expr.name("value")) @@ -4289,7 +4290,7 @@ def _build_numeric_index_fragment( type_str: str, weight_expr: Any, ) -> Any: - import xorq.api as xo + from ._xorq import api as xo return ( base_tbl.select(field_expr.name("value")) @@ -4387,7 +4388,7 @@ def __repr__(self) -> str: @property def values(self) -> FrozenOrderedDict[str, Any]: - import xorq.api as xo + from ._xorq import api as xo return FrozenOrderedDict( { @@ -4435,7 +4436,7 @@ def to_untagged(self): ) if not fields_to_index: - import xorq.api as xo + from ._xorq import api as xo return xo.memtable( { diff --git a/src/boring_semantic_layer/profile.py b/src/boring_semantic_layer/profile.py index 83efbdcb..5d65c709 100644 --- a/src/boring_semantic_layer/profile.py +++ b/src/boring_semantic_layer/profile.py @@ -4,7 +4,7 @@ from pathlib import Path from ibis import BaseBackend -from xorq.vendor.ibis.backends.profiles import Profile as XorqProfile +from ._xorq import Profile as XorqProfile from .utils import read_yaml_file diff --git a/src/boring_semantic_layer/projection_utils.py b/src/boring_semantic_layer/projection_utils.py index 8651be42..bd72d8d6 100644 --- a/src/boring_semantic_layer/projection_utils.py +++ b/src/boring_semantic_layer/projection_utils.py @@ -8,7 +8,7 @@ from attrs import frozen from returns.result import Failure, Success -from xorq.vendor.ibis.expr import types as ir +from ._xorq import types as ir from .graph_utils import walk_nodes diff --git a/src/boring_semantic_layer/query.py b/src/boring_semantic_layer/query.py index 5e214f2e..37d1db5c 100644 --- a/src/boring_semantic_layer/query.py +++ b/src/boring_semantic_layer/query.py @@ -27,7 +27,7 @@ def _get_ibis_api(): xorq does not support, plain ibis is used as the fallback. """ try: - import xorq.api as xo + from ._xorq import api as xo return xo except Exception: diff --git a/src/boring_semantic_layer/serialization/__init__.py b/src/boring_semantic_layer/serialization/__init__.py index 8470ae14..ab5ca285 100644 --- a/src/boring_semantic_layer/serialization/__init__.py +++ b/src/boring_semantic_layer/serialization/__init__.py @@ -86,8 +86,7 @@ def do_convert(xorq_mod: XorqModule): import re - from xorq.common.utils.node_utils import replace_nodes - from xorq.vendor.ibis.expr.operations.relations import DatabaseTable + from .._xorq import DatabaseTable, replace_nodes xorq_table = ibis_expr diff --git a/src/boring_semantic_layer/serialization/reconstruct.py b/src/boring_semantic_layer/serialization/reconstruct.py index 645df8e0..ba06b5f8 100644 --- a/src/boring_semantic_layer/serialization/reconstruct.py +++ b/src/boring_semantic_layer/serialization/reconstruct.py @@ -82,11 +82,13 @@ def _unwrap_cached_nodes(expr): return _unwrap_xorq_wrappers(expr, strip_remote=False) def _reconstruct_table(): - from xorq.common.utils.graph_utils import walk_nodes - from xorq.common.utils.ibis_utils import from_ibis - from xorq.expr.relations import Read - from xorq.vendor import ibis - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from .._xorq import ( + Read, + from_ibis, + ibis, + relations as xorq_rel, + walk_nodes, + ) unwrapped_expr = _unwrap_cached_nodes(xorq_expr) @@ -247,8 +249,7 @@ def _reconstruct_limit( def _reconstruct_join( metadata: dict, xorq_expr, source, context: BSLSerializationContext ): - from xorq.common.utils.graph_utils import walk_nodes - from xorq.vendor.ibis.expr.operations import relations as xorq_rel + from .._xorq import relations as xorq_rel, walk_nodes from .. import expr as bsl_expr @@ -303,7 +304,7 @@ def _reconstruct_join( def _unwrap_xorq_wrappers(expr, *, strip_remote: bool = False): """Walk past Tag, CachedNode, and optionally RemoteTable wrappers.""" - from xorq.expr.relations import CachedNode, RemoteTable, Tag + from .._xorq import CachedNode, RemoteTable, Tag op = expr.op() if isinstance(op, Tag): @@ -319,7 +320,7 @@ def _unwrap_xorq_wrappers(expr, *, strip_remote: bool = False): def _unwrap_join_ref(expr): """If expr is a JoinReference, return the underlying table.""" - from xorq.vendor.ibis.expr.operations.relations import JoinReference + from .._xorq import JoinReference if isinstance(expr.op(), JoinReference): return expr.op().parent.to_expr() @@ -339,7 +340,7 @@ def _rebind_to_backend(expr, target_backend): def _split_join_expr(xorq_expr): """Extract left and right table expressions from a joined xorq expression.""" - from xorq.vendor.ibis.expr.operations.relations import JoinChain + from .._xorq import JoinChain expr = _unwrap_xorq_wrappers(xorq_expr, strip_remote=True) op = expr.op() @@ -373,7 +374,7 @@ def _split_join_expr(xorq_expr): def extract_xorq_metadata(xorq_expr) -> dict[str, Any] | None: """Walk a xorq expression tree to find BSL tag metadata.""" - from xorq.expr.relations import Tag + from .._xorq import Tag @safe def get_op(expr): diff --git a/src/boring_semantic_layer/serialization/tag_handler.py b/src/boring_semantic_layer/serialization/tag_handler.py index 91df1b9b..efdaac3f 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.expr.builders import TagHandler +from .._xorq import TagHandler from . import ( BSLSerializationContext, diff --git a/src/boring_semantic_layer/utils.py b/src/boring_semantic_layer/utils.py index ce5944ce..b83bba55 100644 --- a/src/boring_semantic_layer/utils.py +++ b/src/boring_semantic_layer/utils.py @@ -178,8 +178,7 @@ def _check_closure_vars(fn: Callable) -> Maybe[str]: def _try_ibis_introspection(fn: Callable) -> Maybe[str]: from returns.result import Success - from xorq.vendor.ibis import _ - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred, _ result = fn(_) if not isinstance(result, Deferred): @@ -254,8 +253,7 @@ def do_convert(): from ibis import _ try: - from xorq import api as xo - from xorq.vendor import ibis as xorq_ibis + from ._xorq import api as xo, ibis as xorq_ibis eval_context = { "ibis": ibis, @@ -284,7 +282,7 @@ def do_convert(): def _is_ibis_literal_node(value) -> bool: try: - from xorq.vendor.ibis.expr.operations.generic import Literal + from ._xorq import Literal return isinstance(value, Literal) except ImportError: return False @@ -292,7 +290,7 @@ def _is_ibis_literal_node(value) -> bool: def serialize_resolver(resolver) -> tuple: """Walk a Resolver tree and produce a hashable nested-tuple representation.""" - from xorq.vendor.ibis.common.deferred import ( + from ._xorq import ( Attr, BinaryOperator, Call, @@ -402,7 +400,7 @@ def _resolve_qualname(module_obj, qualname: str): def deserialize_resolver(data: tuple): """Reconstruct a Resolver tree from a nested-tuple representation.""" - from xorq.vendor.ibis.common.deferred import ( + from ._xorq import ( Attr, BinaryOperator, Call, @@ -426,7 +424,7 @@ def deserialize_resolver(data: tuple): return Just(func) case ("ibis_literal", py_value, dtype_str): - from xorq.vendor import ibis + from ._xorq import ibis lit_expr = ibis.literal(py_value, type=ibis.dtype(dtype_str)) return Just(lit_expr.op()) @@ -441,7 +439,7 @@ def deserialize_resolver(data: tuple): case ("call", func_data, args_data, kwargs_data): func_resolver = deserialize_resolver(func_data) args_resolvers = tuple(deserialize_resolver(a) for a in args_data) - from xorq.vendor.ibis.common.collections import FrozenDict + from ._xorq import FrozenDict kwargs_resolvers = FrozenDict( {k: deserialize_resolver(v) for k, v in kwargs_data} ) @@ -483,7 +481,7 @@ def deserialize_resolver(data: tuple): case ("map", type_name, items_data): typ = {"dict": dict}[type_name] - from xorq.vendor.ibis.common.collections import FrozenDict + from ._xorq import FrozenDict values = FrozenDict( {k: deserialize_resolver(v) for k, v in items_data} ) @@ -503,11 +501,11 @@ def _is_deferred(obj) -> bool: def expr_to_structured(fn: Callable) -> Result[tuple, Exception]: """Convert a callable/Deferred expression to a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred as XorqDeferred + from ._xorq import Deferred as XorqDeferred @safe def do_convert(): - from xorq.vendor.ibis import _ + from ._xorq import _ if isinstance(fn, XorqDeferred): return serialize_resolver(fn._resolver) @@ -528,7 +526,7 @@ def do_convert(): def structured_to_expr(data: tuple) -> Result: """Reconstruct a Deferred from a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred @safe def do_convert(): @@ -545,7 +543,7 @@ def join_predicate_to_structured(fn: Callable) -> Result[tuple, Exception]: calling the function with two named Deferred variables (``left``, ``right``) and serializing the resulting resolver tree. """ - from xorq.vendor.ibis.common.deferred import Deferred, Variable + from ._xorq import Deferred, Variable @safe def do_convert(): @@ -566,7 +564,7 @@ def do_convert(): def structured_to_join_predicate(data: tuple) -> Result[Callable, Exception]: """Reconstruct a binary join predicate from a structured tuple representation.""" - from xorq.vendor.ibis.common.deferred import Deferred + from ._xorq import Deferred @safe def do_convert():