diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000..f5542a8 --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,74 @@ +name: Deploy documentation + +# Publishes versioned docs to the `gh-pages` branch using the Zensical-compatible +# fork of `mike` (https://github.com/squidfunk/mike): each version is committed +# as a subdirectory of `site_url` (see zensical.toml), with a `latest` alias and +# root redirect pointing at the newest release. Runs on release tags (v*.*.*), +# and can be triggered manually. + +on: + push: + tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] + workflow_dispatch: + inputs: + version: + description: "Version label to publish (e.g. 0.29 or dev)" + required: true + default: "dev" + alias: + description: "Alias to update" + required: false + default: "latest" + set_default: + description: "Point the site root at this alias" + type: boolean + default: true + +permissions: + contents: write # mike pushes the built docs to the gh-pages branch + +jobs: + deploy: + name: Deploy docs with mike + runs-on: ubuntu-latest + # Auto-deploy only on the canonical repo; manual runs are allowed anywhere + # (e.g. to bootstrap gh-pages on a fork while testing). + if: ${{ github.event_name == 'workflow_dispatch' || github.repository == 'substrait-io/substrait-python' }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # mike reads/writes the full gh-pages history + - name: Install uv with python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + - name: Install docs deps + mike + run: | + uv sync --frozen --group docs + uv pip install "git+https://github.com/squidfunk/mike.git" + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Resolve version and alias + id: v + run: | + if [ "${{ github.event_name }}" = "push" ]; then + full="${GITHUB_REF_NAME#v}" # 0.29.0 + echo "version=${full%.*}" >> "$GITHUB_OUTPUT" # 0.29 (minor series) + echo "alias=latest" >> "$GITHUB_OUTPUT" + echo "set_default=true" >> "$GITHUB_OUTPUT" + else + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" + echo "alias=${{ inputs.alias }}" >> "$GITHUB_OUTPUT" + echo "set_default=${{ inputs.set_default }}" >> "$GITHUB_OUTPUT" + fi + - name: Deploy version + run: | + uv run mike deploy --push --update-aliases \ + "${{ steps.v.outputs.version }}" "${{ steps.v.outputs.alias }}" + - name: Set default version + if: ${{ steps.v.outputs.set_default == 'true' }} + run: | + uv run mike set-default --push "${{ steps.v.outputs.alias }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..7d03ae2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,29 @@ +name: Build documentation + +on: + pull_request: + push: + branches: [ main ] + +permissions: + contents: read + +# Build-check only: this verifies the docs compile on every PR so broken pages +# or a broken mkdocstrings reference are caught early. It does NOT publish. +# Versioned publishing to GitHub Pages is handled separately by +# .github/workflows/docs-deploy.yml (mike -> gh-pages branch). + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Install uv with python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + - name: Build the documentation site + run: | + uv run --frozen --group docs zensical build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ac1315..03be0fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,10 @@ cd substrait-python ``` ## Development environment -Activate environment with uv. +Set up the environment with uv. This installs the default `dev` dependency +group, which includes everything needed to run the tests. ``` -uv sync --extra test +uv sync ``` # Lint & Format @@ -26,3 +27,19 @@ Run tests in the project's root dir. ``` uv run pytest ``` + +# Documentation +The user guide lives in `docs/` and is built with [Zensical](https://zensical.org); +the API reference under `docs/reference/` is generated from the package +docstrings via mkdocstrings. Configuration is in `zensical.toml`. + +Preview it locally with live reload, or build the static site into `./site`: +``` +pixi run docs-serve # or: uv run --group docs zensical serve +pixi run docs-build # or: uv run --group docs zensical build +``` + +When editing docstrings that appear in the reference, prefer Markdown (fenced +code blocks and backticked names) over reStructuredText so they render cleanly. +Every pull request runs a docs build-check; versioned docs are published to +GitHub Pages on release (see `.github/workflows/docs-deploy.yml`). diff --git a/README.md b/README.md index 9587aa0..ef3f38c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,43 @@ This project is not an execution engine for Substrait Plans. ## Status This is an experimental package that is still under development. -# Example +# Building plans with the DataFrame API + +The `substrait.dataframe` module is an ergonomic, fluent API for authoring +Substrait plans — a Polars/PySpark-style DataFrame with operator-overloaded +expressions, on top of the lower-level builders. It is the recommended way to +build plans by hand: + +```python +import substrait.dataframe as sub + +plan = ( + sub.read_named_table("people", {"id": sub.i64, "age": sub.i64, "name": sub.string}) + .filter(sub.col("age") > 25) + .with_columns(adult=sub.col("age") >= 18) + .select("id", "name", "adult") + .to_plan() +) +``` + +`plan` is a `substrait.proto.Plan` ready to hand to a consumer such as DuckDB or +DataFusion. Install the `extensions` extra so function overloads resolve against +the standard Substrait extensions: + +```sh +pip install "substrait[extensions]" +``` + +**📖 See the [documentation guide](docs/index.md)** for the full walkthrough — +data sources, types, expressions, joins, aggregations, window functions, +subqueries, DDL/writes, and custom extensions. Build it locally with +`pixi run docs-serve` (or `uv run zensical serve`). + +# Example (low-level API) + +The examples below construct plans with the raw `substrait.proto` and +`substrait.builders` layers. For most hand-authored plans, prefer the +[DataFrame API](#building-plans-with-the-dataframe-api) above. ## Produce a Substrait Plan The ``substrait.proto`` module provides access to the classes diff --git a/docs/aggregations.md b/docs/aggregations.md new file mode 100644 index 0000000..41b40b9 --- /dev/null +++ b/docs/aggregations.md @@ -0,0 +1,76 @@ +# Aggregations + +Summarize rows with `group_by(...).agg(...)`. Aggregate functions come from the +[`f` namespace](functions.md); name each measure with `.alias(...)`. + +## Group by and aggregate + +```python +import substrait.dataframe as sub + +df.group_by("region").agg( + sub.f.sum(sub.col("amount")).alias("total"), + sub.f.count(sub.col("amount")).alias("n"), +) +``` + +Group by multiple keys by passing several, and group the whole frame (a grand +total) by passing none: + +```python +df.group_by("region", "product").agg(sub.f.sum(sub.col("amount")).alias("total")) +df.group_by().agg(sub.f.count(sub.col("id")).alias("rows")) +``` + +`group_by` keys may be column names or expressions. + +## One-shot form + +`aggregate` does the same thing in a single call — the grouping keys first, then +the measures: + +```python +df.aggregate("region", sub.f.sum(sub.col("amount")).alias("total")) +df.aggregate(["region", "product"], sub.f.count(sub.col("id")).alias("n")) +``` + +## Modifying a measure + +Aggregate measures support several modifiers, which chain: + +```python +sub.f.count(sub.col("customer")).distinct().alias("unique_customers") +sub.f.sum(sub.col("amount")).filter(sub.col("status") == "paid").alias("paid_total") +sub.f.string_agg(sub.col("name"), sub.lit(", ")).order_by("name").alias("names") +``` + +- **`.distinct()`** — operate on distinct inputs (`COUNT(DISTINCT x)`). +- **`.filter(predicate)`** — restrict this aggregate to matching rows + (SQL `agg(x) FILTER (WHERE ...)`). Returns a `Measure`, only meaningful inside + `agg(...)`. +- **`.order_by(*keys, descending=, nulls_last=)`** — order the inputs to an + order-sensitive aggregate. +- **`.alias(name)`** — name the output column. + +## Grouping sets, rollup, and cube + +For multiple grouping levels in one aggregation, pass explicit `grouping_sets` +(lists of key names or positions into the keys), or use the `rollup` / `cube` +shortcuts: + +```python +# explicit grouping sets: by (region, product), by (region), and the grand total +df.group_by("region", "product", grouping_sets=[["region", "product"], ["region"], []]) \ + .agg(sub.f.sum(sub.col("amount")).alias("total")) + +# ROLLUP: (region, product), (region), () +df.rollup("region", "product").agg(sub.f.sum(sub.col("amount")).alias("total")) + +# CUBE: every subset of the keys +df.cube("region", "product").agg(sub.f.sum(sub.col("amount")).alias("total")) +``` + +## Next + +- [Window functions](window-functions.md) — aggregate without collapsing rows. +- [The function namespace](functions.md) — the full set of aggregate functions. diff --git a/docs/consuming-plans.md b/docs/consuming-plans.md new file mode 100644 index 0000000..2775968 --- /dev/null +++ b/docs/consuming-plans.md @@ -0,0 +1,103 @@ +# Consuming plans + +substrait-python **produces** plans; it does not execute them. Once you have a +`proto.Plan`, hand it to a Substrait consumer to run. + +## Materialize and serialize + +`.to_plan()` returns a `substrait.proto.Plan`; `SerializeToString()` turns it +into the bytes a consumer accepts: + +```python +import substrait.dataframe as sub + +plan = ( + sub.read_named_table("customer", {"c_name": sub.string, "c_nationkey": sub.i32}) + .filter(sub.col("c_nationkey") == 3) + .select("c_name") + .to_plan() +) + +payload = plan.SerializeToString() +``` + +`DataFrame.to_substrait(registry=...)` is an alias kept for parity with the +[Narwhals wrapper](#relationship-to-narwhals); it materializes against a given +registry. + +## Inspecting a plan + +The bundled pretty printer renders the plan as a compact tree — far more +readable than the raw protobuf text: + +```python +from substrait.utils.display import pretty_print_plan + +pretty_print_plan(plan, use_colors=True) +``` + +## Handing off to an engine + +The [`examples/`](https://github.com/substrait-io/substrait-python/tree/main/examples) +directory has runnable end-to-end scripts. In short, most engines take the +serialized bytes: + +=== "DuckDB" + + ```python + import duckdb + + duckdb.install_extension("substrait") + duckdb.load_extension("substrait") + duckdb.sql("CALL from_substrait(?)", params=[plan.SerializeToString()]) + ``` + + See [`examples/duckdb_example.py`](https://github.com/substrait-io/substrait-python/blob/main/examples/duckdb_example.py). + +=== "ADBC" + + ```python + import adbc_driver_duckdb.dbapi + + with adbc_driver_duckdb.dbapi.connect(":memory:") as conn, conn.cursor() as cur: + cur.executescript("INSTALL substrait;") + cur.executescript("LOAD substrait;") + cur.execute(plan.SerializeToString()) + print(cur.fetch_arrow_table()) + ``` + + See [`examples/adbc_example.py`](https://github.com/substrait-io/substrait-python/blob/main/examples/adbc_example.py). + +!!! tip "Matching a real table's schema" + The examples read a table's Arrow schema from the engine and convert it via + `pyarrow.substrait.serialize_schema(...).to_pysubstrait().base_schema`, then + feed that `NamedStruct` straight into `read_named_table`. That keeps the plan + schema exactly aligned with what the engine will resolve. + +## Round-tripping + +A serialized plan loads back with the generated protobuf class — useful for +tests and for consuming plans other producers emit: + +```python +from substrait.proto import Plan + +restored = Plan() +restored.ParseFromString(payload) +``` + +## Relationship to Narwhals + +`substrait.dataframe` is the **native** fluent frame you call directly. +`substrait.narwhals` is a separate **integration layer**: it implements the +[Narwhals](https://narwhals-project.org) backend protocol and lets +backend-agnostic Narwhals code compile down to a plan, delegating to the native +frame underneath. Reach for it when you want to drive plan construction through +Narwhals (`nw.from_native(...)`); reach for `substrait.dataframe` when you want +to build plans directly. See +[`examples/narwhals_example.py`](https://github.com/substrait-io/substrait-python/blob/main/examples/narwhals_example.py). + +## Next + +- [Getting started](getting-started.md) — back to the basics. +- [API reference](reference/index.md) — the generated symbol reference. diff --git a/docs/custom-extensions.md b/docs/custom-extensions.md new file mode 100644 index 0000000..187e5d5 --- /dev/null +++ b/docs/custom-extensions.md @@ -0,0 +1,122 @@ +# Custom extensions + +Substrait is extensible: engines define their own functions, types, and +relations via extensions. This page covers using a custom +[`ExtensionRegistry`](#the-registry) and building [user-defined +relations](#extension-relations). + +## The registry + +Every `DataFrame` carries an `ExtensionRegistry`. The default +(`sub.default_registry()`) is preloaded with the standard extensions and shared +across frames. To use custom functions, build your own registry and register +extensions on it: + +```python +import substrait.dataframe as sub + +reg = sub.ExtensionRegistry(load_default_extensions=True) +reg.register_extension_yaml("my_functions.yaml") # from a YAML file +# or, from an already-parsed dict (must contain a "urn" field): +reg.register_extension_dict(definitions) +``` + +Pass the registry to a [source function](data-sources.md) so the whole chain +uses it: + +```python +df = sub.read_named_table("t", {"x": sub.i64}, registry=reg) +``` + +### Reaching custom functions by name + +The global `sub.f` only knows the *default* extensions. For functions from your +registry, build a bound namespace with `functions_for`, or use `df.f` (which is +bound to that frame's registry automatically): + +```python +myf = sub.functions_for(reg) +myf.my_double(sub.col("x")) + +# equivalently, off a frame built with `reg`: +df.f.my_double(sub.col("x")) +``` + +See [The function namespace](functions.md) for more. + +## Extension relations + +When you need a relation Substrait has no built-in for, implement a **detail** +class describing how to (de)serialize its `google.protobuf.Any` payload and how +to derive its output schema. This mirrors substrait-java's +`LeafRelDetail` / `SingleRelDetail` / `MultiRelDetail`. There are three abstract +base classes, by input arity: + +| ABC | Inputs | `derive_schema` receives | +|-----|--------|--------------------------| +| `ExtensionLeafDetail` | 0 | (nothing) | +| `ExtensionSingleDetail` | 1 | the input `Type.Struct` | +| `ExtensionMultiDetail` | N | a list of input `Type.Struct` | + +Each requires `to_any()`, `from_any(cls, detail)`, and `derive_schema(...)`, plus +a `type_url` identifying the payload. + +```python +import substrait.dataframe as sub +import substrait.type_pb2 as stt +from google.protobuf.any_pb2 import Any + + +class MyLeaf(sub.ExtensionLeafDetail): + type_url = "example.com/my.LeafDetail" + + def to_any(self) -> Any: + payload = Any() + payload.type_url = self.type_url + # payload.value = ... serialize your fields ... + return payload + + @classmethod + def from_any(cls, detail: Any) -> "MyLeaf": + return cls() # ... deserialize your fields ... + + def derive_schema(self) -> stt.NamedStruct: + return sub.named_struct(names=["x"], struct=sub.struct([sub.i64.non_null])) +``` + +### Building the relation + +Use the frame verbs / entry point matching the arity: + +```python +# leaf (a source): starts a new DataFrame +df = sub.extension_leaf(MyLeaf()) + +# single-input: applied to an existing frame +df = base.extension(MySingle(...)) + +# multi-input: this frame plus others +df = base.extension_multi([other1, other2], MyMulti(...)) +``` + +`DataFrame.extension` also accepts a raw `google.protobuf.Any` directly, in +which case the input schema is assumed to pass through unchanged. + +### Registering for schema inference + +substrait-python re-derives schemas from the serialized proto, where the detail +is an opaque `Any`. So inference can follow your relation, register the detail +class — then inference reconstructs it from the plan's `Any` and calls +`derive_schema`: + +```python +reg.register_extension_relation(MyLeaf) +``` + +Registration is process-global (type URLs are globally unique), so inference +then works on any plan containing that relation. + +## Next + +- [Data sources](data-sources.md) — `read_extension_table` and `extension_leaf`. +- [Consuming plans](consuming-plans.md) — serialize and hand off the plan. diff --git a/docs/data-sources.md b/docs/data-sources.md new file mode 100644 index 0000000..e202fb0 --- /dev/null +++ b/docs/data-sources.md @@ -0,0 +1,105 @@ +# Data sources + +Every `DataFrame` starts from a source. Each function below returns a +`DataFrame` you can chain verbs onto. All of them accept an optional `registry` +keyword to bind a custom [`ExtensionRegistry`](custom-extensions.md); omit it to +use the shared default. + +Sources take a **schema**, given either as a `{name: type}` dict or a +`proto.NamedStruct`. See [Types & schemas](types-and-schemas.md) for the type +system; in short, `sub.i64` is nullable and `sub.i64.non_null` is required. + +## Named tables + +The most common source — a table the consumer resolves by name (a `ReadRel` +with a `NamedTable`): + +```python +import substrait.dataframe as sub + +people = sub.read_named_table( + "people", {"id": sub.i64.non_null, "name": sub.string, "age": sub.i64} +) +``` + +A multi-part name (catalog / schema / table) is given as a list: + +```python +sub.read_named_table(["main", "public", "people"], {"id": sub.i64}) +``` + +## Inline rows (VALUES) + +`from_records` builds a `VirtualTable` — rows embedded directly in the plan, +like SQL `VALUES`. Rows may be dicts keyed by column name or positional +sequences aligned to the schema; `None` becomes a typed null: + +```python +df = sub.from_records( + [ + {"id": 1, "name": "Ada"}, + {"id": 2, "name": "Alan"}, + (3, None), # positional; name is a typed null + ], + {"id": sub.i64.non_null, "name": sub.string}, +) +``` + +Each value is typed according to its schema column, so `from_records` is handy +for tests and small lookup tables. + +## Files + +Read local files by path (or a list of paths) plus a schema. These build a +`ReadRel` over `LocalFiles`, one entry per path: + +```python +sub.read_parquet("data/events.parquet", {"ts": sub.i64, "kind": sub.string}) +sub.read_orc(["a.orc", "b.orc"], {"x": sub.i64}) +sub.read_arrow("table.arrow", {"x": sub.i64}) +``` + +CSV/TSV reads take a couple of extra knobs: + +```python +sub.read_csv( + "data/people.csv", + {"id": sub.i64, "name": sub.string}, + delimiter=",", # use "\t" for TSV + header_lines_to_skip=1, # skip the header row +) +``` + +!!! note "Schemas are declared, not inferred" + substrait-python builds plans; it does not open your files. You always + supply the schema — the reader in the *consuming* engine is responsible for + matching it against the actual file contents. + +## Custom sources + +For a source your engine understands but Substrait has no built-in relation for, +there are two extension entry points. Both are covered in detail under +[Custom extensions](custom-extensions.md): + +- **`read_extension_table(schema, detail)`** — a `ReadRel` whose source is an + opaque `google.protobuf.Any` (`detail`). You still declare the output schema. + + ```python + sub.read_extension_table({"x": sub.i64}, my_any_detail) + ``` + +- **`extension_leaf(detail)`** — a fully custom leaf relation + (`ExtensionLeafRel`). Here `detail` is an + [`ExtensionLeafDetail`](custom-extensions.md#extension-relations) whose + `derive_schema()` defines the output columns, so no separate schema argument + is needed. + + ```python + sub.extension_leaf(MyLeafDetail(...)) + ``` + +## Next + +You have a `DataFrame` — now shape it. Continue with +[Types & schemas](types-and-schemas.md) or jump to +[Transformations](transformations.md). diff --git a/docs/ddl-and-writes.md b/docs/ddl-and-writes.md new file mode 100644 index 0000000..e404ec7 --- /dev/null +++ b/docs/ddl-and-writes.md @@ -0,0 +1,74 @@ +# DDL & writes + +Beyond read queries, the API builds plans that **create, drop, update, and write +to** tables and views. Each returns a terminal `DataFrame`; call `.to_plan()` to +materialize it. + +## Writing query results to a table + +`write_named_table` turns a query into a write sink (`WriteRel`). `op` is the +write operation and `mode` controls what happens when the table already exists: + +```python +import substrait.dataframe as sub + +summary = ( + sub.read_named_table("orders", {"region": sub.string, "amount": sub.fp64}) + .group_by("region") + .agg(sub.f.sum(sub.col("amount")).alias("total")) +) + +plan = summary.write_named_table("region_totals", op="ctas", mode="replace").to_plan() +``` + +- **`op`** — `ctas` (create-table-as-select, default) or `insert`. +- **`mode`** — behavior if the target exists: `error` (default), `append`, + `replace`, or `ignore`. + +## Create table / view + +```python +# CREATE TABLE region_totals (region string, total fp64) +sub.create_table("region_totals", {"region": sub.string, "total": sub.fp64}) + +# CREATE OR REPLACE +sub.create_table("region_totals", {"region": sub.string}, replace=True) + +# CREATE VIEW backed by a query (a DataFrame) +big_orders = sub.read_named_table("orders", {"amount": sub.fp64}) \ + .filter(sub.col("amount") > 1000) +sub.create_view("big_orders", big_orders) +``` + +## Drop table / view + +Pass `if_exists=True` for the `IF EXISTS` variant: + +```python +sub.drop_table("region_totals") +sub.drop_table("region_totals", if_exists=True) +sub.drop_view("big_orders", if_exists=True) +``` + +## Update + +`update_table` builds an `UPDATE` statement. `assignments` maps a target column +(by name or index) to a new-value expression, applied where `where` holds (all +rows if omitted): + +```python +sub.update_table( + "orders", + {"id": sub.i64, "amount": sub.fp64, "status": sub.string}, + assignments={"amount": sub.col("amount") * 1.1}, + where=sub.col("status") == "pending", +) +``` + +The schema you pass describes the target table so the assignment targets and +`where` predicate can be resolved against it. + +## Next + +- [Consuming plans](consuming-plans.md) — hand the built plan to an engine. +- [Custom extensions](custom-extensions.md) — extend beyond the built-in relations. diff --git a/docs/expressions.md b/docs/expressions.md new file mode 100644 index 0000000..ecbba34 --- /dev/null +++ b/docs/expressions.md @@ -0,0 +1,199 @@ +# Expressions + +An **`Expr`** is a composable, unbound column expression. You build them with +`sub.col`, `sub.lit`, Python operators, and the [`f` function +namespace](functions.md), then pass them to verbs like `.filter`, +`.with_columns`, and `.select`. Nothing resolves immediately — an `Expr` is +resolved lazily against the schema and registry when the plan is materialized. + +## Columns and literals + +```python +import substrait.dataframe as sub + +sub.col("age") # reference a column by name +sub.col(0) # ...or by position +sub.lit(25) # a literal (type inferred: i64) +sub.lit(3.5) # fp64 +sub.lit("draft") # string +sub.lit(None, sub.i64) # a typed null — the type is required for None +``` + +`lit` infers the Substrait type from the Python value (see the table under +[Literal type inference](#literal-type-inference)); pass a second argument to +override it (`sub.lit(25, sub.i32)`). + +## Operators + +`Expr` overloads the Python operators every pandas / Polars / PySpark user +expects. Each maps to a fixed standard function-extension and is resolved +against the registry at build time. + +| Operator | Substrait function | +|----------|--------------------| +| `<` `<=` `>` `>=` | `lt` `lte` `gt` `gte` (comparison) | +| `==` `!=` | `equal` `not_equal` (comparison) | +| `+` `-` `*` `/` `%` `**` | `add` `subtract` `multiply` `divide` `modulus` `power` (arithmetic) | +| `-x` (unary) | `negate` | +| `&` `\|` `~` | `and` `or` `not` (boolean) | + +```python +(sub.col("age") > 25) & sub.col("name").is_not_null() +(sub.col("x") + sub.col("y")) * 2 +-sub.col("balance") +~sub.col("is_active") +``` + +!!! warning "Use `&` `|` `~`, not `and` `or` `not`" + Python's `and`/`or`/`not` keywords cannot be overloaded and would coerce the + expression to a bool. Use the bitwise operators `&`, `|`, `~` — and mind + their higher precedence, so parenthesize comparisons: + `(a > 1) & (b < 2)`. + +!!! note "`Expr` is intentionally unhashable" + Because `==` builds an expression rather than comparing, `Expr` sets + `__hash__ = None` (exactly as pandas/Polars do). An `Expr` cannot be used as + a dict key or set member. + +### Numeric literal coercion + +Substrait does not implicitly coerce mixed numeric operands. To keep the common +cases working, a **bare Python number** on one side of an operator is typed to +match the **column** on the other side at resolve time: + +```python +sub.col("price_fp64") * 2 # the 2 is typed fp64, so multiply resolves +sub.col("count_i32") > 25 # the 25 is typed i32, so gt resolves +``` + +A `float` literal always stays floating point (it is never narrowed into an +integer column). Coercion applies **only to literals** — a genuine +column-to-column type mismatch must be bridged explicitly with +[`cast`](#casting): + +```python +sub.col("small_i32").cast(sub.i64) + sub.col("big_i64") +``` + +Decimal operands are handled too: arithmetic keeps a decimal literal's natural +type, while a comparison requires the literal to fit the column's +`decimal(precision, scale)` exactly (otherwise it raises rather than silently +rounding — wrap it with `sub.lit(value, )` or cast the column). + +## Null and membership tests + +```python +sub.col("x").is_null() +sub.col("x").is_not_null() +sub.col("x").is_nan() +sub.col("x").is_in(["active", "pending"]) # SQL IN +sub.col("x").between(1, 10) # inclusive low <= x <= high +sub.col("x").is_distinct_from(sub.col("y")) # null-safe != +sub.col("x").is_not_distinct_from(sub.col("y")) # null-safe == +``` + +`coalesce` returns the first non-null argument: + +```python +sub.coalesce(sub.col("preferred"), sub.col("fallback"), sub.lit("n/a")) +``` + +!!! note "`between` / `is_in` bounds are not coerced" + Like the [`f.*` helpers](functions.md), these do not coerce bare Python + numbers to the column's type. Pass matching literals or + `sub.lit(..., type)` when the types differ. + +## Conditionals (CASE) + +Chain `when().then()` and finish with `.otherwise()`, PySpark/Polars-style: + +```python +tier = ( + sub.when(sub.col("score") >= 90).then("A") + .when(sub.col("score") >= 80).then("B") + .otherwise("C") +) +df.with_columns(tier=tier) +``` + +For a value-match against literal keys, `Expr.switch` is more compact: + +```python +sub.col("code").switch({1: "one", 2: "two"}, default="other") +``` + +## Casting + +`cast` is the explicit escape hatch when automatic literal coercion is not +enough — most often between two columns of different numeric types: + +```python +sub.col("small_i32").cast(sub.i64) +sub.col("amount").cast(sub.decimal(2, 12)) +``` + +It accepts a `proto.Type` or a bare type builder / `DataType`. + +## Nested field access + +Reach into struct, list, and map columns: + +```python +sub.col("address").struct_field(0) # struct field by position +sub.col("tags").list_element(2) # list element by offset +sub.col("tags")[2] # same, via [] (integer offset only) +sub.col("attrs").map_key("color") # map value by key +``` + +These chain, so `sub.col("a").struct_field(1).list_element(0)` walks a nested +path. + +## Higher-order list functions + +Apply a lambda over the elements of a `list` column. The callback receives an +`Expr` bound to the current element: + +```python +sub.col("xs").list_transform(lambda x: x + 1) # map over elements +sub.col("xs").list_filter(lambda x: x > 0) # keep matching elements +``` + +## Naming + +`.alias(name)` sets the output column name of an expression — essential when a +projection would otherwise produce a generated name: + +```python +df.with_columns(total=sub.col("qty") * sub.col("price")) +df.select(sub.col("qty").alias("quantity")) +``` + +## Literal type inference + +`sub.lit(value)` (and bare Python scalars passed to expressions) map to Substrait +types as follows: + +| Python | Substrait type | +|--------|----------------| +| `bool` | `boolean` | +| `int` | `i64` | +| `float` | `fp64` | +| `decimal.Decimal` | `decimal` (scale/precision from the value) | +| `str` | `string` | +| `bytes` / `bytearray` | `binary` | +| `datetime` (naive) | `precision_timestamp(6)` | +| `datetime` (tz-aware) | `precision_timestamp_tz(6)` | +| `date` | `date` | +| `time` | `precision_time(6)` | +| `uuid.UUID` | `uuid` | + +Anything else raises — wrap it with `sub.lit(value, )`. `bool` is checked +before `int` (since `True` is an `int`), and `datetime` before `date` (since +`datetime` subclasses `date`). + +## Next + +- [The function namespace](functions.md) — everything operators can't express. +- [Transformations](transformations.md) — where expressions are used. +- [Aggregations](aggregations.md) and [Window functions](window-functions.md) — + measures and windowed expressions. diff --git a/docs/functions.md b/docs/functions.md new file mode 100644 index 0000000..de8397d --- /dev/null +++ b/docs/functions.md @@ -0,0 +1,118 @@ +# The function namespace + +Operators cover arithmetic, comparison, and boolean logic. Everything else — +`sum`, `substring`, `coalesce`, `row_number`, and the hundreds of other +functions the Substrait extensions define — lives on the **`f` namespace**: + +```python +import substrait.dataframe as sub + +sub.f.sum(sub.col("amount")) +sub.f.upper(sub.col("name")) +sub.f.coalesce(sub.col("a"), sub.col("b")) +sub.f.row_number() +``` + +Each helper returns an [`Expr`](expressions.md), so it composes with operators +and other functions freely. + +## It is generated from the registry + +`f` is not a hand-curated list. It is built lazily on first access by +enumerating every function — scalar, aggregate, and window — defined by the +loaded extension registry. So it always matches the `substrait-extensions` +version you have installed, and it covers all three function kinds. + +Discover what's available with `dir` / tab-completion, and membership-test with +`in`: + +```python +dir(sub.f) # sorted list of every function name +"sum" in sub.f # True +``` + +## Hidden plumbing + +A raw Substrait scalar function call needs both an extension URN *and* a +signature name. `f` hides both — `sub.f.sum(...)` figures out the extension and +resolves the concrete overload from the argument types at build time. + +**Multi-extension names.** Some names appear in more than one extension (e.g. +`add` in `functions_arithmetic`, `functions_arithmetic_decimal`, and +`functions_datetime`). For those, the correct extension is chosen at resolve +time from the actual argument types, preferring the base extension over its +`decimal` / `approx` variants. + +**Keyword names.** The three function names that are Python keywords are exposed +with a trailing underscore — `sub.f.and_`, `sub.f.or_`, `sub.f.not_` — and remain +reachable via `getattr(sub.f, "and")`. + +## Literals are *not* coerced here + +This is the key difference from [operators](expressions.md#numeric-literal-coercion). +Operators coerce a bare Python literal to the peer column's type; the `f.*` +helpers do **not**. Pass typed operands or a `lit(...)` when a specific overload +is required: + +```python +# operator: the 2 is coerced to the column's type +sub.col("price_fp64") * 2 + +# f.* helper: pass a typed operand so the fp64 overload resolves +sub.f.multiply(sub.col("price_fp64"), 2.0) +sub.f.multiply(sub.col("price_fp64"), sub.lit(2, sub.fp64)) +``` + +This bites most often when a function's overload is typed narrower than the +default `i64`. For example, `substring` expects `i32` offsets, so a bare `1`/`3` +(inferred `i64`) fails to resolve — pass `i32` literals: + +```python +# sub.f.substring(sub.col("name"), 1, 3) # no substring(string, i64, i64) overload +sub.f.substring(sub.col("name"), sub.lit(1, sub.i32), sub.lit(3, sub.i32)) +``` + +If no overload matches the argument types, resolution raises an error naming the +function and the signature it tried. + +## Aggregate and window functions + +The same namespace provides aggregate and window functions. Aggregates are used +inside [`group_by().agg()`](aggregations.md); window functions become windowed +expressions with [`.over(...)`](window-functions.md): + +```python +# aggregate +df.group_by("region").agg(sub.f.sum(sub.col("amount")).alias("total")) + +# window +df.with_columns(rn=sub.f.row_number().over(partition_by="region", order_by="ts")) +``` + +Aggregate measures also support `.alias()`, `.distinct()`, `.order_by(...)`, and +`.filter(...)` — see [Aggregations](aggregations.md). + +## Function options + +Some functions accept configurable behaviors (e.g. overflow handling). Pass them +as keyword arguments to the helper; they are attached as function options: + +```python +sub.f.add(sub.col("a"), sub.col("b"), overflow="ERROR") +``` + +## Custom extensions + +The global `sub.f` knows only the *default* extensions. To reach functions from +your own registered extensions, build a namespace bound to a specific registry: + +```python +reg = sub.ExtensionRegistry(load_default_extensions=True) +reg.register_extension_yaml("my_functions.yaml") + +myf = sub.functions_for(reg) +myf.my_double(sub.col("x")) +``` + +A `DataFrame` built with that registry also exposes it as `df.f`, so +`df.f.my_double(...)` just works. See [Custom extensions](custom-extensions.md). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..2a06a3d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,100 @@ +--- +icon: lucide/rocket +--- + +# Getting started + +## Install + +```sh +pip install "substrait[extensions]" +``` + +The `extensions` extra pulls in the pieces needed to resolve function overloads +against the standard Substrait extensions (the ANTLR-based type-derivation +parser and `pyyaml`). The ergonomic API leans on this for its +[function namespace](functions.md) and operator resolution, so install it unless +you have a reason not to. + +With conda/mamba: + +```sh +conda install -c conda-forge python-substrait +``` + +## Your first plan + +Everything is reachable from a single import. The convention used throughout +this guide is: + +```python +import substrait.dataframe as sub +``` + +Build a plan by chaining verbs off a data source and finishing with `.to_plan()`: + +```python +import substrait.dataframe as sub + +plan = ( + sub.read_named_table("people", {"id": sub.i64, "age": sub.i64, "name": sub.string}) + .filter(sub.col("age") > 25) + .with_columns(next_year=sub.col("age") + 1) + .select("id", "name", "next_year") + .to_plan() +) +``` + +`plan` is a `substrait.proto.Plan` — the same protobuf message you would get from +the raw builders, just built far more concisely. This one describes: + +1. a **read** of the `people` table with a three-column schema, +2. a **filter** keeping rows where `age > 25`, +3. a **projection** adding `next_year = age + 1`, then +4. a **select** narrowing the output to three columns. + +## Inspecting the result + +A `proto.Plan` prints as protobuf text, which is verbose. For a compact, +readable tree, use the bundled pretty printer: + +```python +from substrait.utils.display import pretty_print_plan + +pretty_print_plan(plan, use_colors=True) +``` + +You can also serialize it to bytes to hand to a consumer (see +[Consuming plans](consuming-plans.md)): + +```python +payload = plan.SerializeToString() +``` + +## Two things to know up front + +### Nullability is explicit + +Schema types are [`DataType`](types-and-schemas.md) objects. A bare `sub.i64` is +**nullable** (the safe default); `sub.i64.non_null` is required: + +```python +sub.read_named_table("sales", {"region": sub.string.non_null, "amount": sub.fp64}) +``` + +### The registry is handled for you + +A `DataFrame` carries an [`ExtensionRegistry`](custom-extensions.md) — by default +a shared one preloaded with the standard extensions +(`sub.default_registry()`). That is why you never pass a registry to +`.filter(...)` or `sub.f.sum(...)`; it is threaded through automatically and +applied when you call `.to_plan()`. To use custom extensions, construct a +registry yourself and pass it to the source function — see +[Custom extensions](custom-extensions.md). + +## Where next + +- [Data sources](data-sources.md) — every way to start a `DataFrame`. +- [Types & schemas](types-and-schemas.md) — the type system and nullability. +- [Expressions](expressions.md) — operators, literals, and column expressions. +- [Transformations](transformations.md) — `select`, `filter`, `sort`, and friends. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ee6b915 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,77 @@ +--- +icon: lucide/home +--- + +# substrait-python + +Python bindings for [Substrait](https://substrait.io) — the cross-language +specification for data compute operations. This site documents the **ergonomic +DataFrame API** (`substrait.dataframe`) for authoring Substrait plans in Python. + +```python +import substrait.dataframe as sub + +plan = ( + sub.read_named_table("people", {"id": sub.i64, "age": sub.i64, "name": sub.string}) + .filter(sub.col("age") > 25) + .with_columns(adult=sub.col("age") >= 18) + .select("id", "name", "adult") + .to_plan() +) +``` + +That expression builds a complete `substrait.proto.Plan` — a read, a filter, a +projection and an output mapping — that any Substrait consumer (DuckDB, +DataFusion, …) can execute. No engine is bundled here: substrait-python +*produces and manipulates* plans; it does not run them. + +## The three layers + +substrait-python offers three ways to build a plan, from lowest- to +highest-level. They compose — the higher layers are thin, faithful facades over +the lower ones and can be freely mixed. + +| Layer | Import | What it is | +|-------|--------|------------| +| **Raw protobuf** | `substrait.proto` | Construct `proto.Plan(...)` field by field. Complete but verbose. | +| **Builders** | `substrait.builders.{plan,extended_expression,type}` | Free functions returning "unbound" callables you materialize with `plan(registry)`. | +| **DataFrame** | `substrait.dataframe` | A fluent, Polars/PySpark-style frame + operator-overloaded expressions. **This guide.** | + +There is also `substrait.narwhals`, a [Narwhals](https://narwhals-project.org) +integration layer that lets backend-agnostic Narwhals code compile to a plan by +driving the native DataFrame underneath. See +[Consuming plans](consuming-plans.md) for how the pieces relate. + +!!! note "Why `substrait.dataframe` and not `substrait`?" + `substrait` is a [PEP 420 namespace package](https://peps.python.org/pep-0420/) + shared with the `substrait-protobuf` distribution. A top-level + `substrait/__init__.py` would shadow `substrait.algebra_pb2` and friends, so + the ergonomic API lives in its own clearly-owned submodule, + `substrait.dataframe`, alongside `substrait.builders`, `substrait.sql`, and + `substrait.narwhals`. + +## Mental model + +- **A `DataFrame` is a lazy, immutable plan builder.** Every verb + (`.filter`, `.select`, `.join`, …) returns a *new* `DataFrame` wrapping a + larger plan; nothing is evaluated until you call `.to_plan()`. +- **An `Expr` is an unbound expression.** `sub.col("age") > 25` does not resolve + a function overload immediately — it is resolved lazily, against the schema and + an [`ExtensionRegistry`](custom-extensions.md), when the plan is materialized. +- **The registry is carried for you.** A `DataFrame` holds an `ExtensionRegistry` + (a shared default preloaded with the standard extensions) so you never thread + it through calls. `.to_plan()` binds everything and returns a `proto.Plan`. + +## Where to go next + +- :material-rocket-launch: **[Getting started](getting-started.md)** — install and + build your first plan. +- :material-table: **[Data sources](data-sources.md)** — tables, files, inline + rows, custom sources. +- :material-function-variant: **[Expressions](expressions.md)** and + **[the function namespace](functions.md)** — build column expressions. +- :material-set-merge: **[Joins](joins.md)**, + **[Aggregations](aggregations.md)**, **[Window functions](window-functions.md)** — + the relational verbs. +- :material-book-open-variant: **[API reference](reference/index.md)** — generated + from the docstrings. diff --git a/docs/joins.md b/docs/joins.md new file mode 100644 index 0000000..126d2c3 --- /dev/null +++ b/docs/joins.md @@ -0,0 +1,97 @@ +# Joins + +Combine two `DataFrame`s. The logical `join` is what you want most of the time; +the physical variants (`hash_join`, `merge_join`, `nested_loop_join`) exist for +when you need to pin a specific algorithm. + +## Logical join + +`join` takes another DataFrame, an `on` expression, and a `how` string: + +```python +import substrait.dataframe as sub + +customers = sub.read_named_table("customers", {"cust_id": sub.i64, "name": sub.string}) +orders = sub.read_named_table( + "orders", {"order_id": sub.i64, "cust_ref": sub.i64, "amount": sub.fp64} +) + +joined = customers.join( + orders, + on=sub.col("cust_id") == sub.col("cust_ref"), + how="inner", +) +``` + +The `on` expression is evaluated against the **concatenation** of the left and +right schemas — columns from both inputs are referenced by name. An optional +`post_filter` predicate is applied to the join output: + +```python +customers.join(orders, on=sub.col("cust_id") == sub.col("cust_ref"), + post_filter=sub.col("amount") > 100) +``` + +### Join types + +`how` accepts all twelve `JoinRel` variants: + +| `how` | Meaning | +|-------|---------| +| `inner` | rows with a match on both sides | +| `outer` | all rows from both sides | +| `left` / `right` | all rows from one side, matched where possible | +| `left_semi` / `right_semi` | rows from one side that have a match (no right columns) | +| `left_anti` / `right_anti` | rows from one side that have **no** match | +| `left_single` / `right_single` | at most one match per row (runtime error on multiple) | +| `left_mark` / `right_mark` | append a nullable-boolean column flagging whether a partner exists | + +## Cross join + +The Cartesian product — every left row paired with every right row: + +```python +customers.cross_join(regions) +``` + +## Column disambiguation + +Substrait references columns positionally, so overlapping column names across +the two inputs are kept as-is; nothing is auto-suffixed. Disambiguate explicitly +with [`rename`](transformations.md#renaming-and-dropping) / `drop` on either +input before joining, or on the result: + +```python +left = customers.rename({"id": "cust_id"}) +right = orders.rename({"id": "order_id"}) +left.join(right, on=sub.col("cust_id") == sub.col("cust_ref")) +``` + +## Physical joins + +When you want to dictate the execution strategy rather than leave it to the +consumer, use the physical variants. All of them accept the same `how` values as +`join`. + +**Nested-loop join** evaluates an arbitrary predicate over the Cartesian product +(the only physical join that supports non-equi conditions): + +```python +customers.nested_loop_join(orders, on=sub.col("cust_id") == sub.col("cust_ref"), + how="inner") +``` + +**Hash join** and **merge join** are equi-joins on key columns. `left_on` / +`right_on` are column names or indices; `right_on` defaults to `left_on`. Merge +join assumes its inputs are already sorted on the keys: + +```python +customers.hash_join(orders, left_on="cust_id", right_on="cust_ref") +customers.merge_join(orders, left_on=["a", "b"], right_on=["x", "y"], how="left") +``` + +## Next + +- [Aggregations](aggregations.md) — summarize the joined rows. +- [Set operations](set-operations.md) — union / intersect / except. +- [Subqueries](subqueries.md) — correlated and uncorrelated subquery expressions. diff --git a/docs/parameters-and-context.md b/docs/parameters-and-context.md new file mode 100644 index 0000000..e1a3ca4 --- /dev/null +++ b/docs/parameters-and-context.md @@ -0,0 +1,52 @@ +# Parameters & context variables + +## Dynamic parameters + +`parameter` builds a placeholder whose value is supplied at execution time — the +plan-level analog of a SQL bind parameter (`?` / `:name`). `index` is the +0-based position bound via the plan's parameter bindings; `type` is a +`proto.Type` or a bare type builder: + +```python +import substrait.dataframe as sub + +orders = sub.read_named_table("orders", {"amount": sub.fp64, "region": sub.string}) + +# keep rows above a threshold provided at runtime +orders.filter(sub.col("amount") > sub.parameter(0, sub.fp64)) +``` + +Give it a readable name with the optional `alias`: + +```python +sub.parameter(0, sub.fp64, alias="min_amount") +``` + +## Execution context variables + +These resolve to values from the execution environment, not from the data: + +```python +sub.current_timestamp() # the query's execution timestamp (precision_timestamp_tz) +sub.current_date() # the query's execution date +sub.current_timezone() # the query's execution timezone (a string) +``` + +`current_timestamp` takes an optional `precision` (default `6`, +microseconds); all three take an optional `alias`: + +```python +orders.with_columns(loaded_at=sub.current_timestamp(precision=3, alias="loaded_at")) +``` + +A typical use is stamping rows or filtering by "today": + +```python +events = sub.read_named_table("events", {"ts": sub.date, "kind": sub.string}) +events.filter(sub.col("ts") == sub.current_date()) +``` + +## Next + +- [Expressions](expressions.md) — combine these with operators and functions. +- [DDL & writes](ddl-and-writes.md) — statements that consume a built plan. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..16abe64 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,83 @@ +--- +icon: lucide/book-open +--- + +# API reference + +Generated from the docstrings of the `substrait.dataframe` package. For a +task-oriented introduction, start with the [guide](../getting-started.md). + +## Entry points & facade + +::: substrait.dataframe + options: + show_root_heading: false + members: false + +## The DataFrame + +::: substrait.dataframe.frame + options: + heading_level: 3 + members: + - DataFrame + - GroupBy + - read_named_table + - from_records + - read_parquet + - read_csv + - read_orc + - read_arrow + - read_extension_table + - extension_leaf + - create_table + - create_view + - drop_table + - drop_view + - update_table + - default_registry + +## Expressions + +::: substrait.dataframe.expr + options: + heading_level: 3 + members: + - Expr + - Measure + - When + - col + - lit + - outer + - when + - coalesce + - parameter + - current_timestamp + - current_date + - current_timezone + - scalar_subquery + - exists + - unique + - any_ + - all_ + - infer_literal_type + +## The function namespace + +::: substrait.dataframe.functions + options: + heading_level: 3 + members: + - functions_for + +## Data types + +::: substrait.dataframe.dtypes + options: + heading_level: 3 + +## Extension relations + +::: substrait.dataframe.extension_relations + options: + heading_level: 3 diff --git a/docs/set-operations.md b/docs/set-operations.md new file mode 100644 index 0000000..dee8aae --- /dev/null +++ b/docs/set-operations.md @@ -0,0 +1,52 @@ +# Set operations + +Combine the rows of several `DataFrame`s that share a schema. All inputs must +have the same column types as the frame you call the method on. + +## Union + +Concatenate rows. The default keeps duplicates (`UNION ALL`); pass +`distinct=True` for a set `UNION`: + +```python +import substrait.dataframe as sub + +q1 = sub.read_named_table("sales_q1", {"region": sub.string, "amount": sub.fp64}) +q2 = sub.read_named_table("sales_q2", {"region": sub.string, "amount": sub.fp64}) + +q1.union(q2) # UNION ALL (keeps duplicates) +q1.union(q2, distinct=True) # UNION (deduplicated) +``` + +You can pass more than one other frame: + +```python +q1.union(q2, q3, q4) +``` + +## Intersect + +Rows present in this frame **and** in every other. Defaults to the deduplicated +`INTERSECT`; pass `distinct=False` for `INTERSECT ALL`: + +```python +active.intersect(subscribed) +active.intersect(subscribed, distinct=False) +``` + +## Except + +Rows in this frame that are **not** in any of the others (SQL `EXCEPT`). +Defaults to deduplicated; pass `distinct=False` for `EXCEPT ALL`. The method is +`except_` (with a trailing underscore, since `except` is a Python keyword): + +```python +all_users.except_(banned_users) +all_users.except_(banned_users, distinct=False) +``` + +## Next + +- [Joins](joins.md) — combine columns rather than rows. +- [Subqueries](subqueries.md) — set membership as an expression (`is_in`, + `in_subquery`, `exists`). diff --git a/docs/subqueries.md b/docs/subqueries.md new file mode 100644 index 0000000..e3cc5b0 --- /dev/null +++ b/docs/subqueries.md @@ -0,0 +1,84 @@ +# Subqueries + +A subquery is another `DataFrame` used *inside an expression*. substrait-python +supports the full range of Substrait subquery forms — scalar, `IN`, `EXISTS`, +quantified comparisons, and correlated references. + +## Scalar subquery + +`scalar_subquery` yields the single value of a one-row, one-column subquery — use +it anywhere a scalar expression is expected: + +```python +import substrait.dataframe as sub + +orders = sub.read_named_table("orders", {"id": sub.i64, "amount": sub.fp64}) +avg_amount = orders.aggregate((), sub.f.avg(sub.col("amount")).alias("avg")) + +# rows above the overall average +orders.filter(sub.col("amount") > sub.scalar_subquery(avg_amount)) +``` + +## IN a subquery + +`Expr.in_subquery` tests membership against a subquery producing a single +column (`x IN (SELECT ...)`): + +```python +customers = sub.read_named_table("customers", {"id": sub.i64}) +recent = sub.read_named_table("recent_orders", {"cust_id": sub.i64}) + +customers.filter(sub.col("id").in_subquery(recent)) +``` + +For membership against a fixed set of literals, use +[`is_in`](expressions.md#null-and-membership-tests) instead. + +## EXISTS and UNIQUE + +`exists` is true when the subquery returns any row; `unique` is true when it has +no duplicate rows: + +```python +customers.filter(sub.exists(recent)) +customers.filter(sub.unique(recent)) +``` + +These are most useful *correlated* — see below. + +## Quantified comparisons (ANY / ALL) + +Compare a value against every row of a subquery with `any_` / `all_`, used on +the right-hand side of a comparison operator: + +```python +# amount greater than ANY row of the subquery (i.e. greater than the minimum) +orders.filter(sub.col("amount") > sub.any_(threshold_df)) + +# amount greater than ALL rows (i.e. greater than the maximum) +orders.filter(sub.col("amount") > sub.all_(threshold_df)) +``` + +## Correlated subqueries + +A correlated subquery references a column from the **enclosing** query. Use +`outer(name)` for that reference. `steps_out` counts nesting levels outward +(`0` = the immediately enclosing query, the default): + +```python +outer_df = sub.read_named_table("customers", {"id": sub.i64, "region": sub.string}) +inner_df = sub.read_named_table("orders", {"cust_id": sub.i64}) + +# customers who have at least one order (correlated EXISTS) +outer_df.filter( + sub.exists(inner_df.filter(sub.col("cust_id") == sub.outer("id"))) +) +``` + +Inside the inner frame, `sub.col("cust_id")` refers to the inner schema and +`sub.outer("id")` refers to the correlated `id` column from `outer_df`. + +## Next + +- [Expressions](expressions.md) — the non-subquery expression forms. +- [Aggregations](aggregations.md) — build the scalar subqueries you compare against. diff --git a/docs/transformations.md b/docs/transformations.md new file mode 100644 index 0000000..8c37730 --- /dev/null +++ b/docs/transformations.md @@ -0,0 +1,115 @@ +# Transformations + +These verbs reshape a `DataFrame`'s rows and columns. Each returns a new +`DataFrame`, so they chain. They accept [`Expr`](expressions.md) objects, bare +column-name strings, and (where sensible) Python scalars. + +## Projecting columns + +`select` **replaces** the projection with exactly the columns you name; +`with_columns` **appends** new columns to the existing ones (Polars naming): + +```python +import substrait.dataframe as sub + +df.select("id", "name") # keep only these two +df.select(sub.col("qty").alias("quantity")) # rename via alias + +df.with_columns(next_year=sub.col("age") + 1) # append a computed column +df.with_columns(sub.col("a"), doubled=sub.col("a") * 2) # positional + named +``` + +With `with_columns`, keyword arguments name the new columns; positional +arguments pass expressions through unchanged (alias them yourself if needed). + +## Renaming and dropping + +```python +df.rename({"old": "new", "qty": "quantity"}) # rename some; others pass through +df.drop("debug", "internal") # keep the rest, in order +``` + +Both resolve the input schema, so an unknown column name raises rather than +silently doing nothing. + +## Filtering rows + +`filter` keeps rows where the predicate is true: + +```python +df.filter(sub.col("age") > 25) +df.filter((sub.col("age") > 25) & sub.col("name").is_not_null()) +``` + +## Sorting + +`sort` orders rows by one or more columns. `descending` and `nulls_last` are +each either a single bool (applied to every key) or a per-column list matching +the columns: + +```python +df.sort("amount") # ascending, nulls last +df.sort("amount", descending=True) +df.sort("region", "amount", descending=[False, True]) +df.sort("amount", nulls_last=False) +``` + +## Limiting and paging + +```python +df.limit(10) # first 10 rows +df.limit(10, offset=20) # 10 rows starting after the first 20 +df.head(5) # alias for limit(5) +df.offset(100) # skip the first 100, keep the rest +``` + +`top_n` fuses an order-by and a limit into a single `TopNRel` — the efficient +way to ask for "the N largest/smallest": + +```python +df.top_n(10, by="amount", descending=True) +df.top_n(10, by=["region", "amount"], descending=[False, True], with_ties=True) +``` + +`with_ties` keeps rows tied with the n-th; `offset` is also supported. + +## Unpivot (wide to long) + +`unpivot` turns a set of columns into `variable`/`value` rows (an `ExpandRel`), +Polars-style. `index` columns are repeated on every output row, and the `on` +columns must share a type: + +```python +df.unpivot( + on=["jan", "feb", "mar"], + index="region", + variable_name="month", + value_name="sales", +) +``` + +## Hints + +`hint` attaches non-semantic annotations to the current relation +(`RelCommon.Hint`) — optimizer statistics and naming. They are purely advisory +and do not change results: + +```python +df.hint(row_count=1_000_000, record_size=64, alias="big_scan") +df.hint(output_names=["a", "b", "c"]) +``` + +## Physical distribution + +For engines that model partitioning, `ExchangeRel` verbs redistribute rows: + +```python +df.repartition(8) # round-robin into 8 partitions +df.broadcast() # broadcast every row to all partitions +``` + +## Next + +- [Joins](joins.md) — combine two DataFrames. +- [Aggregations](aggregations.md) — `group_by().agg()` and friends. +- [Set operations](set-operations.md) — union / intersect / except. diff --git a/docs/types-and-schemas.md b/docs/types-and-schemas.md new file mode 100644 index 0000000..1bbd583 --- /dev/null +++ b/docs/types-and-schemas.md @@ -0,0 +1,103 @@ +# Types & schemas + +## Schemas + +Every source takes a schema. The ergonomic form is a plain dict mapping column +names to types: + +```python +import substrait.dataframe as sub + +sub.read_named_table("people", {"id": sub.i64.non_null, "name": sub.string}) +``` + +Anywhere a schema is accepted you may also pass a `proto.NamedStruct` built with +`sub.named_struct(...)` / `sub.struct(...)`, which is what the dict is converted +into under the hood. The dict form preserves insertion order, so column order is +exactly what you write. + +## Nullability is explicit + +The no-argument type shortcuts (`sub.i64`, `sub.string`, …) are `DataType` +objects, not bare builder functions. This makes nullability a visible choice +rather than a silent default (inspired by substrait-java's `N`/`R` +`TypeCreator` constants): + +| Form | Nullability | +|------|-------------| +| `sub.i64` | nullable (the safe default when used bare in a schema) | +| `sub.i64.nullable` | explicitly nullable | +| `sub.i64.non_null` | required / non-nullable | +| `sub.i64()` | callable, nullable — parity with the builder layer | +| `sub.i64(nullable=False)` | callable, required | + +A `DataType` is callable and yields a `proto.Type`, so a bare `sub.i64` works +anywhere a zero-argument type builder is expected (schema dicts, +[`lit`](expressions.md), [`cast`](expressions.md), [`parameter`](parameters-and-context.md)): + +```python +{"a": sub.i64, "b": sub.string.non_null, "c": sub.fp64.nullable} +``` + +## The type catalogue + +Every concrete Substrait data type is reachable from `sub`. + +### No-argument types (`DataType` shortcuts) + +`boolean`, `i8`, `i16`, `i32`, `i64`, `fp32`, `fp64`, `string`, `binary`, +`date`, `uuid`, `interval_year`. + +Each supports `.nullable` / `.non_null` / call syntax as above. + +### Parametrized types (builder functions) + +These take their parameters plus a `nullable` keyword (defaulting to `True`): + +```python +sub.decimal(2, 10) # decimal(scale=2, precision=10) — scale first! +sub.varchar(255) # variable-length string, max length 255 +sub.fixed_char(3) # fixed-length string +sub.fixed_binary(16) # fixed-length binary +sub.precision_timestamp(6) # microsecond timestamp (no tz) +sub.precision_timestamp_tz(6) # microsecond timestamp with tz +sub.precision_time(6) # microsecond time-of-day +sub.interval_day(6) # day/second interval +sub.interval_compound(6) # year-month + day-second interval + +sub.struct([sub.i64(), sub.string()]) # a struct of two fields +sub.list_(sub.i64()) # list (list_ avoids shadowing built-in list) +sub.map_(sub.string(), sub.i64()) # map (map_ avoids shadowing built-in map) +sub.named_struct(names=["a", "b"], struct=sub.struct([sub.i64(), sub.string()])) + +sub.user_defined(...) # a user-defined type from an extension +``` + +!!! warning "`decimal` takes `scale` before `precision`" + The signature is `decimal(scale, precision, nullable=True)`, matching the + lower-level builder. So `sub.decimal(2, 10)` is `decimal(10, 2)` in + SQL notation (precision 10, scale 2). + +!!! note "Nesting nullable parametrized types" + Pass the *inner* types with their own nullability, e.g. + `sub.list_(sub.i64.non_null, nullable=False)` for a required list of + required `i64`. Bare `DataType` shortcuts default to nullable when called. + +Deprecated kinds (`timestamp` / `time` / `timestamp_tz`, superseded by the +`precision_*` family) and non-data-type kinds (function, alias) are intentionally +not surfaced. + +## Types vs. literals + +A schema declares column *types*. To build a typed *value* — a constant in an +expression — use [`lit`](expressions.md), which infers the type from the Python +value or takes one explicitly: + +```python +sub.lit(42) # inferred i64 +sub.lit(42, sub.i32) # explicit i32 +sub.lit(None, sub.string) # a typed null (type is required for None) +``` + +See [Expressions](expressions.md) for how literal types interact with operators +and overload resolution. diff --git a/docs/window-functions.md b/docs/window-functions.md new file mode 100644 index 0000000..88b6644 --- /dev/null +++ b/docs/window-functions.md @@ -0,0 +1,67 @@ +# Window functions + +A window function computes an aggregate-like value **without collapsing rows** — +each row gets a result computed over a related set of rows. Take a window +function from the [`f` namespace](functions.md) and turn it into a windowed +expression with `.over(...)`: + +```python +import substrait.dataframe as sub + +df.with_columns( + rn=sub.f.row_number().over(partition_by="region", order_by="ts"), +) +``` + +This is the SQL `OVER (PARTITION BY region ORDER BY ts)`. + +!!! note "`.over()` needs a *window* function" + `.over(...)` applies to the functions the registry classifies as window + functions — `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`, + `ntile`, `lag`, `lead`, `first_value`, `last_value`, `nth_value`. Plain + aggregates like `sum`/`avg` are *not* window functions here, so + `sub.f.sum(...).over(...)` raises. To get a running/moving aggregate over a + frame, use `first_value` / `last_value` / `nth_value` as shown below. + +## Partitioning and ordering + +`partition_by` and `order_by` each take a single column name/expression or a +list of them. `descending` / `nulls_last` control the ordering: + +```python +sub.f.rank().over( + partition_by=["region", "product"], + order_by="amount", + descending=True, +) +``` + +Both are optional — omit `partition_by` for a single window over all rows, and +omit `order_by` for an unordered window. + +## Frames + +A frame narrows the set of rows the function sees within each partition. Give it +as `rows=(start, end)` (physical row offsets) or `range=(start, end)` (value +offsets); specify at most one. Each endpoint is: + +- `None` — unbounded (to the start/end of the partition), +- `0` — the current row, +- a negative int — that many rows **preceding**, +- a positive int — that many rows **following**. + +```python +# the latest amount seen so far: start of partition through the current row +sub.f.last_value(sub.col("amount")).over(order_by="ts", rows=(None, 0)) + +# value from the immediately preceding row within a 3-row window +sub.f.first_value(sub.col("amount")).over(order_by="ts", rows=(-1, 1)) + +# the final amount in the partition: current row onward +sub.f.last_value(sub.col("amount")).over(order_by="ts", rows=(0, None)) +``` + +## Next + +- [Aggregations](aggregations.md) — collapse groups into one row each. +- [The function namespace](functions.md) — the available window functions. diff --git a/pixi.lock b/pixi.lock index fd08427..06d9225 100644 --- a/pixi.lock +++ b/pixi.lock @@ -172,7 +172,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f9/04/98c216967275cd9a3e517dfeeb513ebff3183f1f22da29180c479c749ac8/sqloxide-0.1.56-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -214,7 +214,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/de/77/dd27f6e325537126ba0b142fdce63bde4305681662ac58e8e779e3c87635/sqloxide-0.1.56-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl @@ -249,7 +249,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/10/c7/79f57728f300079148ac4e4b988000dcbd1f58960040db89594424a58336/sqloxide-0.1.56.tar.gz @@ -283,7 +283,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/94/fd/268d58f8feb4d206d2896dae5e8a4a17c671c9cbb6b15c6e6300d2e168a2/sqloxide-0.1.56-cp313-cp313-macosx_11_0_arm64.whl @@ -320,13 +320,267 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c8/5c/487e081a5ac1a8538f7eb3a3e3ea04c39da13acde1f17916ca1767d40c4e/sqloxide-0.1.56-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/bf/46b67f14ce68abbf6eca701530fe03f4eeda4318564f154ad6de48495474/substrait_antlr-0.96.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + docs: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.1-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-hf4e2dac_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.14.10-h4196e79_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.1-hb1525cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45-default_h1979696_105.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-hd65408f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.1-h10b116e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.0-h8e36d6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.11-h4c0d347_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.14.10-hc0dabaa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h561c983_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.1-h14c5de8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-hd09e2f1_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.14.10-hb17bafe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.14.10-hb0cad00_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.14.10-h37e10c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/15/dfd8f80938e968fff8a9df3990563b4f0775f7fdb5cc965c4d10f4c730f0/substrait_extensions-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/ca/1efad490d213b499ff076801187ee38a5ee17ea6eecbc230929c4ae12807/substrait_protobuf-0.96.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl extensions: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -725,6 +979,13 @@ packages: purls: [] size: 146519 timestamp: 1767500828366 +- pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + name: click + version: 8.4.2 + sha256: e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl name: colorama version: 0.4.6 @@ -804,6 +1065,22 @@ packages: - pytest-cov~=6.0.0 ; extra == 'test' - python-dotenv~=1.0.0 ; extra == 'test' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl + name: deepmerge + version: 2.1.0 + sha256: 8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75 + requires_dist: + - typing-extensions ; python_full_version < '3.10' + - validate-pyproject[all] ; extra == 'dev' + - pyupgrade ; extra == 'dev' + - black ; extra == 'dev' + - mypy ; extra == 'dev' + - pytest ; extra == 'dev' + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/3f/3d/ce68db53084746a4a62695a4cb064e44ce04123f8582bb3afbf6ee944e16/duckdb-1.2.2-cp313-cp313-win_amd64.whl name: duckdb version: 1.2.2 @@ -829,6 +1106,25 @@ packages: version: 1.2.2 sha256: 88916d7f0532dc926bed84b50408c00dcbe6d2097d0de93c3ff647d8d57b4f83 requires_python: '>=3.7.0' +- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + name: ghp-import + version: 2.1.0 + sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 + requires_dist: + - python-dateutil>=2.8.1 + - twine ; extra == 'dev' + - markdown ; extra == 'dev' + - flake8 ; extra == 'dev' + - wheel ; extra == 'dev' +- pypi: https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl + name: griffelib + version: 2.1.0 + sha256: cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00 + requires_dist: + - pip>=24.0 ; extra == 'pypi' + - platformdirs>=4.2 ; extra == 'pypi' + - wheel>=0.42 ; extra == 'pypi' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.1-h33c6efd_0.conda sha256: 7d6463d0be5092b2ae8f2fad34dc84de83eab8bd44cc0d4be8931881c973c48f md5: 518e9bbbc3e3486d6a4519192ba690f8 @@ -867,6 +1163,14 @@ packages: version: 2.3.0 sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + name: jinja2 + version: 3.1.6 + sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + requires_dist: + - markupsafe>=2.0 + - babel>=2.7 ; extra == 'i18n' + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda sha256: 1027bd8aa0d5144e954e426ab6218fd5c14e54a98f571985675468b339c808ca md5: 3ec0aa5037d39b06554109a01e6fb0c6 @@ -1328,6 +1632,131 @@ packages: purls: [] size: 55476 timestamp: 1727963768015 +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + name: markdown + version: 3.10.2 + sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 + requires_dist: + - coverage ; extra == 'testing' + - pyyaml ; extra == 'testing' + - mkdocs>=1.6 ; extra == 'docs' + - mkdocs-nature>=0.6 ; extra == 'docs' + - mdx-gh-links>=0.2 ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: markupsafe + version: 3.0.3 + sha256: 133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + name: markupsafe + version: 3.0.3 + sha256: 9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + name: markupsafe + version: 3.0.3 + sha256: 116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + name: mergedeep + version: 1.3.4 + sha256: 70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + name: mkdocs + version: 1.6.1 + sha256: db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e + requires_dist: + - click>=7.0 + - colorama>=0.4 ; sys_platform == 'win32' + - ghp-import>=1.0 + - importlib-metadata>=4.4 ; python_full_version < '3.10' + - jinja2>=2.11.1 + - markdown>=3.3.6 + - markupsafe>=2.0.1 + - mergedeep>=1.3.4 + - mkdocs-get-deps>=0.2.0 + - packaging>=20.5 + - pathspec>=0.11.1 + - pyyaml-env-tag>=0.1 + - pyyaml>=5.1 + - watchdog>=2.0 + - babel>=2.9.0 ; extra == 'i18n' + - babel==2.9.0 ; extra == 'min-versions' + - click==7.0 ; extra == 'min-versions' + - colorama==0.4 ; sys_platform == 'win32' and extra == 'min-versions' + - ghp-import==1.0 ; extra == 'min-versions' + - importlib-metadata==4.4 ; python_full_version < '3.10' and extra == 'min-versions' + - jinja2==2.11.1 ; extra == 'min-versions' + - markdown==3.3.6 ; extra == 'min-versions' + - markupsafe==2.0.1 ; extra == 'min-versions' + - mergedeep==1.3.4 ; extra == 'min-versions' + - mkdocs-get-deps==0.2.0 ; extra == 'min-versions' + - packaging==20.5 ; extra == 'min-versions' + - pathspec==0.11.1 ; extra == 'min-versions' + - pyyaml-env-tag==0.1 ; extra == 'min-versions' + - pyyaml==5.1 ; extra == 'min-versions' + - watchdog==2.0 ; extra == 'min-versions' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + name: mkdocs-autorefs + version: 1.4.4 + sha256: 834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 + requires_dist: + - markdown>=3.3 + - markupsafe>=2.0.1 + - mkdocs>=1.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + name: mkdocs-get-deps + version: 0.2.2 + sha256: e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 + requires_dist: + - importlib-metadata>=4.3 ; python_full_version < '3.10' + - mergedeep>=1.3.4 + - platformdirs>=2.2.0 + - pyyaml>=5.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl + name: mkdocstrings + version: 1.0.6 + sha256: 2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7 + requires_dist: + - jinja2>=3.1 + - markdown>=3.6 + - markupsafe>=1.1 + - mkdocs>=1.6 + - mkdocs-autorefs>=1.4 + - pymdown-extensions>=6.3 + - mkdocstrings-crystal>=0.3.4 ; extra == 'crystal' + - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' + - mkdocstrings-python>=1.16.2 ; extra == 'python' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl + name: mkdocstrings-python + version: 2.0.5 + sha256: 30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d + requires_dist: + - mkdocstrings>=0.30 + - mkdocs-autorefs>=1.4 + - griffelib>=2.0 + - typing-extensions>=4.0 ; python_full_version < '3.11' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -1444,6 +1873,20 @@ packages: version: '26.0' sha256: b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + requires_dist: + - hyperscan>=0.7 ; extra == 'hyperscan' + - typing-extensions>=4 ; extra == 'optional' + - google-re2>=1.1 ; extra == 're2' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + name: platformdirs + version: 4.10.0 + sha256: fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl name: pluggy version: 1.6.0 @@ -1500,13 +1943,22 @@ packages: version: 22.0.0 sha256: ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl name: pygments - version: 2.19.2 - sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + version: 2.20.0 + sha256: 81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' - requires_python: '>=3.8' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl + name: pymdown-extensions + version: 11.0.1 + sha256: db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 + requires_dist: + - markdown>=3.6 + - pyyaml + - pygments>=2.19.1 ; extra == 'extra' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl name: pytest version: 9.0.2 @@ -1652,6 +2104,13 @@ packages: size: 16617922 timestamp: 1765019627175 python_site_packages_path: Lib/site-packages +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda build_number: 8 sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 @@ -1688,6 +2147,13 @@ packages: version: 6.0.3 sha256: 8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + name: pyyaml-env-tag + version: '1.1' + sha256: 17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 + requires_dist: + - pyyaml + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 md5: d7d95fc8287ea7bf33e0e7116d2b95ec @@ -1809,6 +2275,11 @@ packages: - pkg:pypi/ruff?source=compressed-mapping size: 11908812 timestamp: 1766095035171 +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' - pypi: https://files.pythonhosted.org/packages/10/c7/79f57728f300079148ac4e4b988000dcbd1f58960040db89594424a58336/sqloxide-0.1.56.tar.gz name: sqloxide version: 0.1.56 @@ -1914,6 +2385,31 @@ packages: purls: [] size: 3472313 timestamp: 1763055164278 +- pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + name: tomli + version: 2.4.1 + sha256: 36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: tomli + version: 2.4.1 + sha256: f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + name: tomli + version: 2.4.1 + sha256: eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: tomli + version: 2.4.1 + sha256: c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + name: tomli + version: 2.4.1 + sha256: 8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c md5: ad659d0a2b3e47e38d829aa8cad2d610 @@ -1968,6 +2464,111 @@ packages: purls: [] size: 115235 timestamp: 1767320173250 +- pypi: https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl + name: watchdog + version: 6.0.0 + sha256: 76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl + name: watchdog + version: 6.0.0 + sha256: 7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + name: watchdog + version: 6.0.0 + sha256: 20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + name: watchdog + version: 6.0.0 + sha256: cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: watchdog + version: 6.0.0 + sha256: a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: zensical + version: 0.0.50 + sha256: a64f957f94f7d8e8847a7f1e8205509ba4b188c93c157c5e7be49bcb8556a127 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: zensical + version: 0.0.50 + sha256: 2bbf3034a2cb1a9d2a72a5ee7047688717c93da0d8a90d25dd871db8d5f33a6d + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl + name: zensical + version: 0.0.50 + sha256: 376887def6470c57509b48519870f74e2b987e30509cf5c819fcf372771f9403 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl + name: zensical + version: 0.0.50 + sha256: 26dda9dbacab84db2743726f83202798e1893fbfddecb6a845812d7a331043ad + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl + name: zensical + version: 0.0.50 + sha256: 2f054769bf96ae46353de3eca2463ad4db6df1da9fde515c6d7fc54c3608e615 + requires_dist: + - click>=8.1.8 + - deepmerge>=2.0 + - jinja2>=3.1 + - markdown>=3.7 + - pygments>=2.20 + - pymdown-extensions>=10.21.3 + - pyyaml>=6.0.2 + - tomli>=2.4.0 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 diff --git a/pyproject.toml b/pyproject.toml index fc2452b..d5f9767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ sql = ["sqloxide", "deepdiff"] [dependency-groups] dev = ["pytest >= 7.0.0", "substrait-antlr==0.96.0", "pyyaml", "sqloxide", "deepdiff", "duckdb<=1.2.2; python_version < '3.14'", "datafusion"] +docs = ["zensical", "mkdocstrings-python"] [tool.pytest.ini_options] pythonpath = "src" @@ -45,11 +46,16 @@ platforms = ["win-64", "linux-64", "osx-64", "osx-arm64", "linux-aarch64"] dev = { features = ["dev"], solve-group = "default" } extensions = { features = ["extensions"], solve-group = "default" } sql = { features = ["sql"], solve-group = "default" } +docs = { features = ["docs"], solve-group = "default" } [tool.pixi.tasks] format = "ruff format" lint = "ruff check" lint_fix = "ruff check --fix" +[tool.pixi.feature.docs.tasks] +docs-build = "zensical build" +docs-serve = "zensical serve" + [tool.pixi.dependencies] ruff = ">=0.14.10,<0.15" diff --git a/src/substrait/dataframe/__init__.py b/src/substrait/dataframe/__init__.py index a3d3f64..4d4f14f 100644 --- a/src/substrait/dataframe/__init__.py +++ b/src/substrait/dataframe/__init__.py @@ -1,16 +1,18 @@ """Ergonomic front door for substrait-python. -A single, shallow import that gets you productive:: +A single, shallow import that gets you productive: - import substrait.dataframe as sub +```python +import substrait.dataframe as sub - plan = ( - sub.read_named_table("people", {"id": sub.i64, "age": sub.i64, "name": sub.string}) - .filter(sub.col("age") > 25) - .with_columns(adult=sub.col("age") >= 18) - .select("id", "name") - .to_plan() - ) +plan = ( + sub.read_named_table("people", {"id": sub.i64, "age": sub.i64, "name": sub.string}) + .filter(sub.col("age") > 25) + .with_columns(adult=sub.col("age") >= 18) + .select("id", "name") + .to_plan() +) +``` This is the Substrait-*native* fluent DataFrame/Expr API -- the higher-level counterpart to the lower-level ``substrait.builders`` layer, and a sibling to diff --git a/src/substrait/dataframe/dtypes.py b/src/substrait/dataframe/dtypes.py index 867544f..67d4c3e 100644 --- a/src/substrait/dataframe/dtypes.py +++ b/src/substrait/dataframe/dtypes.py @@ -3,13 +3,15 @@ The lower-level ``substrait.builders.type`` builders take a ``nullable`` keyword that defaults to ``True``, which is easy to apply silently. ``DataType`` wraps a primitive builder so nullability is explicit and reads well -- inspired by -substrait-java's ``N`` (nullable) / ``R`` (required) ``TypeCreator`` constants:: - - sub.i64 # bare: nullable (the safe default) when used in a schema - sub.i64.nullable # explicitly nullable - sub.i64.non_null # required / non-nullable - sub.i64() # still callable, for parity with the builder layer - sub.i64(nullable=False) +substrait-java's ``N`` (nullable) / ``R`` (required) ``TypeCreator`` constants: + +```python +sub.i64 # bare: nullable (the safe default) when used in a schema +sub.i64.nullable # explicitly nullable +sub.i64.non_null # required / non-nullable +sub.i64() # still callable, for parity with the builder layer +sub.i64(nullable=False) +``` A ``DataType`` is callable, so anywhere a zero-arg type builder is accepted (schema dicts, ``lit``) a bare ``sub.i64`` keeps working and yields a nullable diff --git a/src/substrait/dataframe/expr.py b/src/substrait/dataframe/expr.py index e254435..a41402e 100644 --- a/src/substrait/dataframe/expr.py +++ b/src/substrait/dataframe/expr.py @@ -3,11 +3,13 @@ ``Expr`` wraps the existing "unbound" expression callables produced by ``substrait.builders.extended_expression`` and adds Python operator overloading so that expressions can be written the way users of pandas / Polars / PySpark / -Ibis expect:: +Ibis expect: - col("age") > 25 - (col("x") + col("y")) * 2 - col("a").is_null() & col("b") +```python +col("age") > 25 +(col("x") + col("y")) * 2 +col("a").is_null() & col("b") +``` Each operator maps to a fixed standard function-extension URN + signature name and defers to the existing ``scalar_function`` builder, which already resolves @@ -237,7 +239,7 @@ def _resolve_over_urns( Builds with the first URN whose overload matches the operands' signature and raises a uniform error if none do. Shared by the operator path - (:func:`_numeric_binary`) and the ``f.*`` namespace's multi-URN helper + (`_numeric_binary`) and the ``f.*`` namespace's multi-URN helper (``substrait.dataframe.functions._multi_urn_helper``) so both resolve identically and their error text cannot drift apart. """ @@ -275,7 +277,7 @@ def _numeric_binary( ``[functions_arithmetic, functions_arithmetic_decimal]`` so decimal operands fall through to the decimal extension when the base one has no overload, mirroring the ``f.*`` namespace's multi-URN resolution. ``decimal_literal`` - (``"natural"`` / ``"peer"``) is forwarded to :func:`_match_numeric_type` to + (``"natural"`` / ``"peer"``) is forwarded to `_match_numeric_type` to pick how a decimal literal is coerced against a decimal peer. """ urns = [urns] if isinstance(urns, str) else list(urns) @@ -333,7 +335,7 @@ def as_bound(value, is_expr): class _SubqueryReduction: - """A subquery wrapped by :func:`any_` / :func:`all_`, consumed by a + """A subquery wrapped by `any_` / `all_`, consumed by a comparison operator to build a ``left ANY/ALL (subquery)`` expression.""" __slots__ = ("reduction_op", "plan") @@ -629,7 +631,7 @@ def make(base_schema, registry): # -- higher-order list functions -------------------------------------- def _higher_order(self, function: str, callback) -> "Expr": """Apply a list higher-order function whose lambda ``callback`` receives - an :class:`Expr` bound to the current list element.""" + an `Expr` bound to the current list element.""" list_unbound = self._unbound def resolve(base_schema, registry): @@ -703,9 +705,11 @@ def list_filter(self, fn) -> "Expr": return self._higher_order("filter", fn) def switch(self, cases: dict, default: Any) -> "Expr": - """Value-match CASE against literal keys:: + """Value-match CASE against literal keys: - col("code").switch({1: "one", 2: "two"}, default="other") + ```python + col("code").switch({1: "one", 2: "two"}, default="other") + ``` Keys must be Python scalars (they become literals); each value may be an ``Expr`` or a scalar. @@ -832,9 +836,11 @@ def filter(self, predicate: Any) -> "Measure": """Restrict this aggregate to rows where ``predicate`` holds (SQL ``agg(x) FILTER (WHERE predicate)``). - Returns a :class:`Measure`, only meaningful inside ``group_by().agg(...)``:: + Returns a `Measure`, only meaningful inside ``group_by().agg(...)``: - f.sum(col("amount")).filter(col("status") == "paid") + ```python + f.sum(col("amount")).filter(col("status") == "paid") + ``` """ return Measure(self, Expr._coerce(predicate)) @@ -842,9 +848,11 @@ def cast(self, type: Any) -> "Expr": """Cast this expression to ``type`` (a proto.Type or a type builder). The explicit escape hatch when automatic literal coercion is not enough, - e.g. between two columns of different numeric types:: + e.g. between two columns of different numeric types: - col("small_i32").cast(sub.i64) + col("big_i64") + ```python + col("small_i32").cast(sub.i64) + col("big_i64") + ``` """ if callable(type): # allow a bare builder / DataType, e.g. sub.i64 type = type() @@ -868,7 +876,7 @@ def __repr__(self) -> str: # pragma: no cover - debugging aid class Measure: """An aggregate expression paired with a ``FILTER (WHERE ...)`` predicate. - Produced by :meth:`Expr.filter` and consumed by + Produced by `Expr.filter` and consumed by ``DataFrame.group_by(...).agg(...)``; not a standalone expression. """ @@ -889,7 +897,7 @@ def order_by(self, *keys: Any, **kwargs: Any) -> "Measure": class When: - """Intermediate for building a CASE expression; see :func:`when`.""" + """Intermediate for building a CASE expression; see `when`.""" __slots__ = ("_clauses", "_pending") @@ -917,12 +925,14 @@ def otherwise(self, default: Any) -> Expr: def when(condition: Any) -> When: - """Begin a CASE expression, PySpark/Polars-style:: + """Begin a CASE expression, PySpark/Polars-style: - when(col("x") > 0).then("pos").when(col("x") < 0).then("neg").otherwise("zero") + ```python + when(col("x") > 0).then("pos").when(col("x") < 0).then("neg").otherwise("zero") + ``` Chain ``.then(value)`` after each ``.when(condition)`` and finish with - ``.otherwise(default)``, which returns the :class:`Expr`. + ``.otherwise(default)``, which returns the `Expr`. """ return When([], Expr._coerce(condition)) @@ -1018,9 +1028,11 @@ def col(name: Union[str, int]) -> Expr: def outer(name: Union[str, int], steps_out: int = 0) -> Expr: """Reference a column from an enclosing query (a correlated reference). - Only valid inside a subquery, e.g. a correlated ``exists``:: + Only valid inside a subquery, e.g. a correlated ``exists``: - outer_df.filter(sub.exists(inner_df.filter(sub.col("k") == sub.outer("k")))) + ```python + outer_df.filter(sub.exists(inner_df.filter(sub.col("k") == sub.outer("k")))) + ``` ``steps_out`` counts query-nesting levels outward (0 = the immediately enclosing query). diff --git a/src/substrait/dataframe/frame.py b/src/substrait/dataframe/frame.py index 38d30f0..a1ef6c8 100644 --- a/src/substrait/dataframe/frame.py +++ b/src/substrait/dataframe/frame.py @@ -5,22 +5,24 @@ Daft's own native frame). It is a thin, chainable wrapper over the ``substrait.builders.plan`` functions: it carries an ``ExtensionRegistry`` so it does not have to be threaded through every call, and it takes -:class:`~substrait.dataframe.expr.Expr` objects (or bare column names / Python -scalars) rather than raw ``scalar_function`` invocations:: +`Expr` objects (or bare column names / Python +scalars) rather than raw ``scalar_function`` invocations: - import substrait.dataframe as sub +```python +import substrait.dataframe as sub - plan = ( - sub.read_named_table("people", {"id": sub.i64, "age": sub.i64}) - .filter(sub.col("age") > 25) - .select("id") - .to_plan() - ) +plan = ( + sub.read_named_table("people", {"id": sub.i64, "age": sub.i64}) + .filter(sub.col("age") > 25) + .select("id") + .to_plan() +) +``` Verb naming follows Polars: ``select`` replaces the projection, ``with_columns`` appends. -Relationship to :mod:`substrait.narwhals`: that module is the **Narwhals +Relationship to `substrait.narwhals`: that module is the **Narwhals integration layer** -- a compliant wrapper that lets ``narwhals`` drive plan construction (``nw.from_native(...)``). It adapts Narwhals calls down onto this native frame; the two layers compose rather than compete. @@ -163,7 +165,7 @@ class DataFrame: """The Substrait-native fluent DataFrame. Build plans directly (``df.filter(...).select(...).to_plan()``). For the - Narwhals-driven equivalent, see :class:`substrait.narwhals.DataFrame`, which + Narwhals-driven equivalent, see `substrait.narwhals.DataFrame`, which wraps this frame to satisfy the Narwhals backend protocol. """ @@ -325,7 +327,7 @@ def top_n( ) -> "DataFrame": """The top ``n`` rows ordered by ``by`` (a fused sort + fetch, TopNRel). - ``descending``/``nulls_last`` follow :meth:`sort`; ``with_ties`` keeps + ``descending``/``nulls_last`` follow `sort`; ``with_ties`` keeps rows tied with the n-th. """ keys = [by] if isinstance(by, (str, Expr)) else list(by) @@ -395,7 +397,7 @@ def nested_loop_join( ) -> "DataFrame": """Physical nested-loop join: evaluate ``on`` over the Cartesian product. - ``how`` accepts the same values as :meth:`join`. + ``how`` accepts the same values as `join`. """ if how not in _JOIN_TYPES: raise ValueError( @@ -435,7 +437,7 @@ def hash_join( """Physical hash equi-join on key columns. ``left_on``/``right_on`` are column names/indices; ``right_on`` defaults - to ``left_on``. ``how`` accepts the same values as :meth:`join`. + to ``left_on``. ``how`` accepts the same values as `join`. """ return self._equi_join( _plan.hash_join, stalg.HashJoinRel, other, left_on, right_on, how @@ -465,7 +467,7 @@ def extension(self, detail: Any) -> "DataFrame": """Apply a custom single-input relation (ExtensionSingleRel). ``detail`` is an - :class:`~substrait.dataframe.extension_relations.ExtensionSingleDetail` (its + `ExtensionSingleDetail` (its ``derive_schema`` defines the output) or a raw ``google.protobuf.Any`` (the input schema is then assumed to pass through). Register the detail class via ``ExtensionRegistry.register_extension_relation`` for schema @@ -478,7 +480,7 @@ def extension_multi( ) -> "DataFrame": """A custom multi-input relation (ExtensionMultiRel) over this frame and ``others``; ``detail`` is an - :class:`~substrait.dataframe.extension_relations.ExtensionMultiDetail`.""" + `ExtensionMultiDetail`.""" inputs = [self._plan, *(o._plan for o in others)] return self._next(_plan.extension_multi(inputs, detail)) @@ -670,7 +672,7 @@ def extension_leaf( """Start a DataFrame from a custom leaf relation (ExtensionLeafRel). ``detail`` is an - :class:`~substrait.dataframe.extension_relations.ExtensionLeafDetail`; its + `ExtensionLeafDetail`; its ``derive_schema`` defines the source's output columns. """ return DataFrame(_plan.extension_leaf(detail), registry) diff --git a/src/substrait/dataframe/functions.py b/src/substrait/dataframe/functions.py index 9db205c..92a7858 100644 --- a/src/substrait/dataframe/functions.py +++ b/src/substrait/dataframe/functions.py @@ -2,18 +2,20 @@ ``f`` is a namespace covering *every* function defined by the Substrait default extensions -- scalar, aggregate and window -- so anything the specification -ships is reachable by name and hides the extension-URN / signature plumbing:: +ships is reachable by name and hides the extension-URN / signature plumbing: - import substrait.dataframe as sub +```python +import substrait.dataframe as sub - sub.f.sum(sub.col("amount")) - sub.f.substring(sub.col("name"), 1, 3) - sub.f.coalesce(sub.col("a"), sub.col("b")) - sub.f.row_number() +sub.f.sum(sub.col("amount")) +sub.f.upper(sub.col("name")) +sub.f.coalesce(sub.col("a"), sub.col("b")) +sub.f.row_number() +``` -Each helper returns an :class:`~substrait.dataframe.expr.Expr`. The namespace is +Each helper returns an `Expr`. The namespace is built lazily on first attribute access from -:func:`substrait.dataframe.frame.default_registry`, +`substrait.dataframe.frame.default_registry`, and supports ``dir(sub.f)`` for discovery/tab-completion. Some function names appear in more than one extension (e.g. ``add`` in @@ -123,7 +125,7 @@ class _FunctionNamespace: """Lazily-populated namespace of a registry's functions. With no registry it enumerates the default extensions; pass a registry (see - :func:`functions_for`) to expose custom extensions registered on it too. + `functions_for`) to expose custom extensions registered on it too. """ def __init__(self, registry=None): @@ -161,12 +163,14 @@ def functions_for(registry) -> _FunctionNamespace: Unlike the global ``f`` (which only knows the default extensions), this surfaces every function on ``registry`` -- including custom extensions - registered via ``register_extension_yaml`` / ``register_extension_dict``:: - - reg = ExtensionRegistry(load_default_extensions=True) - reg.register_extension_yaml("my_functions.yaml") - myf = sub.functions_for(reg) - myf.my_double(sub.col("x")) + registered via ``register_extension_yaml`` / ``register_extension_dict``: + + ```python + reg = ExtensionRegistry(load_default_extensions=True) + reg.register_extension_yaml("my_functions.yaml") + myf = sub.functions_for(reg) + myf.my_double(sub.col("x")) + ``` """ return _FunctionNamespace(registry) diff --git a/uv.lock b/uv.lock index db2ed63..ce89986 100644 --- a/uv.lock +++ b/uv.lock @@ -134,6 +134,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/1b/31cf2449da9a296f6c6c0002c7ae91a25c3a4bfef071763bbeb85300b402/cachebox-5.2.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70c718f6bb77e6ba142b9a055b81ce85412a0c0e5e82a154489b45e6f91d09ec", size = 287614, upload-time = "2026-04-10T12:21:47.909Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "cloudpickle" version = "3.1.2" @@ -188,6 +200,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/26/4a2bad8eb430d8d805a4642c4bff25103a37548d74ab346f8b1e024abcc5/deepdiff-9.1.0-py3-none-any.whl", hash = "sha256:80c0460e1993b04f6f0ca79abf25548b129fd218478c4ebb08f80560f5d10610", size = 184662, upload-time = "2026-05-15T20:18:03.956Z" }, ] +[[package]] +name = "deepmerge" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, +] + [[package]] name = "duckdb" version = "1.2.2" @@ -240,6 +261,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -249,6 +291,205 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, +] + [[package]] name = "orderly-set" version = "5.5.0" @@ -267,6 +508,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -350,11 +609,24 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -375,6 +647,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -439,6 +723,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sqloxide" version = "0.61.1" @@ -518,6 +823,10 @@ dev = [ { name = "sqloxide" }, { name = "substrait-antlr" }, ] +docs = [ + { name = "mkdocstrings-python" }, + { name = "zensical" }, +] [package.metadata] requires-dist = [ @@ -541,6 +850,10 @@ dev = [ { name = "sqloxide" }, { name = "substrait-antlr", specifier = "==0.96.0" }, ] +docs = [ + { name = "mkdocstrings-python" }, + { name = "zensical" }, +] [[package]] name = "substrait-antlr" @@ -637,3 +950,65 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/79/f7959b13c766a831f1248779bb718555f70f40c5b8e68db7de1de9936662/zensical-0.0.50.tar.gz", hash = "sha256:7040e52ebe5e6a275e4edeb351bf2bc314d007f3fb5750f178a38d840723e69c", size = 3979246, upload-time = "2026-07-09T13:52:11.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:26dda9dbacab84db2743726f83202798e1893fbfddecb6a845812d7a331043ad", size = 12817706, upload-time = "2026-07-09T13:51:29.624Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:376887def6470c57509b48519870f74e2b987e30509cf5c819fcf372771f9403", size = 12691061, upload-time = "2026-07-09T13:51:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbf3034a2cb1a9d2a72a5ee7047688717c93da0d8a90d25dd871db8d5f33a6d", size = 13129505, upload-time = "2026-07-09T13:51:36.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/50d02402e53aa01a5c5c116a3522c7bb18d6ae4da964d8b236173b6fa086/zensical-0.0.50-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26fe4167663d544ca385ef420a49d3b738c7606a79d7a62a6bedcbf74cd383e7", size = 13055300, upload-time = "2026-07-09T13:51:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0f/45f4f3e56a6483ea9f88478b20d6b434718e7c46579239589202e5135ec1/zensical-0.0.50-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8813d1f8895b19520d4a982d6bfcfd06372d73d96e6a63f0d2cf8f3b6036d5a1", size = 13445447, upload-time = "2026-07-09T13:51:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a64f957f94f7d8e8847a7f1e8205509ba4b188c93c157c5e7be49bcb8556a127", size = 13098775, upload-time = "2026-07-09T13:51:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/46/65/1e42e18d8f9a0cb9fcc8b871852810c7fe196237b79aaa831e863325091e/zensical-0.0.50-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:781d333bcb42c6a6b92360e05eadc944e2674794e1a95537971de1bf64cb9f89", size = 13305922, upload-time = "2026-07-09T13:51:51.107Z" }, + { url = "https://files.pythonhosted.org/packages/50/36/9cace194ba13aef3bdde8ca2d327f3a05cdf5c456e94f6d896852e2cbd38/zensical-0.0.50-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5d0f86eec76c23efb04566cd444bf025845ac8a02e75c8a6b4af13a39a2a9955", size = 13325488, upload-time = "2026-07-09T13:51:54.883Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/2891a9316e486794dae43151561b49f3313499cfb2c95c7886abad7bcf2d/zensical-0.0.50-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:392a7299a3c4d1ac7ff288bbed181d77a9fd23e6614180476e01630aaee0d67e", size = 13496866, upload-time = "2026-07-09T13:51:58.257Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/fbd3af58081a0d399fa913ef1f2fd6a007f2f06f7026c8d6a6096e34b996/zensical-0.0.50-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cac3448d8c8a76dfb43e55908b4ca9ad17596aee89a9287a184c1a0ba7dce2a2", size = 13437565, upload-time = "2026-07-09T13:52:01.766Z" }, + { url = "https://files.pythonhosted.org/packages/d3/79/709311ff202326260ff223fb6e9ddefa3100474c428e3c70a04036cefe0e/zensical-0.0.50-cp310-abi3-win32.whl", hash = "sha256:32b244f96a930f68e049b2e03d50790c120ef701b6277ddd854905a33040c75f", size = 12371966, upload-time = "2026-07-09T13:52:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl", hash = "sha256:2f054769bf96ae46353de3eca2463ad4db6df1da9fde515c6d7fc54c3608e615", size = 12604832, upload-time = "2026-07-09T13:52:09.013Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..f9c46ea --- /dev/null +++ b/zensical.toml @@ -0,0 +1,143 @@ +# Zensical configuration for the substrait-python documentation. +# Docs: https://zensical.org/docs/ +# +# Build: pixi run docs-build (or: uv run zensical build) +# Serve: pixi run docs-serve (or: uv run zensical serve) + +[project] +site_name = "substrait-python" +site_description = "Python bindings for Substrait, with an ergonomic DataFrame API for authoring plans." +site_author = "Substrait contributors" +# Canonical hosting URL. Required for multi-version docs: each version is +# published as a subdirectory of this URL. Adjust if the site is served +# elsewhere (custom domain, or a fork's *.github.io while testing). +site_url = "https://substrait-io.github.io/substrait-python/" +repo_url = "https://github.com/substrait-io/substrait-python" +repo_name = "substrait-io/substrait-python" +copyright = "Copyright © 2026 Substrait contributors" + +nav = [ + { "Home" = "index.md" }, + { "Getting started" = "getting-started.md" }, + { "Guide" = [ + { "Data sources" = "data-sources.md" }, + { "Types & schemas" = "types-and-schemas.md" }, + { "Expressions" = "expressions.md" }, + { "The function namespace" = "functions.md" }, + { "Transformations" = "transformations.md" }, + { "Joins" = "joins.md" }, + { "Aggregations" = "aggregations.md" }, + { "Set operations" = "set-operations.md" }, + { "Window functions" = "window-functions.md" }, + { "Subqueries" = "subqueries.md" }, + { "Parameters & context" = "parameters-and-context.md" }, + { "DDL & writes" = "ddl-and-writes.md" }, + { "Custom extensions" = "custom-extensions.md" }, + { "Consuming plans" = "consuming-plans.md" }, + ] }, + { "API reference" = "reference/index.md" }, +] + +# -------------------------------------------------------------------------- +# Multi-version documentation (via the Zensical-compatible `mike` fork). +# Publishing is done by .github/workflows/docs-deploy.yml, which commits each +# version to the `gh-pages` branch as a subdirectory of site_url. Setting the +# provider here renders the version selector in the header. +# -------------------------------------------------------------------------- +[project.extra.version] +provider = "mike" # enables the selector; reads mike's versions.json on gh-pages +default = "latest" # the alias the site root redirects to +alias = true + +# -------------------------------------------------------------------------- +# Theme (Material-style; ships with Zensical). +# -------------------------------------------------------------------------- +[project.theme] +language = "en" +features = [ + "content.code.annotate", + "content.code.copy", + "content.tabs.link", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.follow", +] + +[[project.theme.palette]] +scheme = "default" +primary = "indigo" +accent = "indigo" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +primary = "indigo" +accent = "indigo" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/substrait-io/substrait-python" + +[[project.extra.social]] +icon = "fontawesome/brands/python" +link = "https://pypi.org/project/substrait/" + +# -------------------------------------------------------------------------- +# Plugins +# -------------------------------------------------------------------------- +# Auto-generated API reference from the package docstrings. +# Install: mkdocstrings-python (in the `docs` dependency group). +[project.plugins.mkdocstrings.handlers.python] +inventories = ["https://docs.python.org/3/objects.inv"] +paths = ["src"] + +[project.plugins.mkdocstrings.handlers.python.options] +docstring_style = "sphinx" +show_source = false +show_root_heading = true +show_symbol_type_heading = true +show_symbol_type_toc = true +members_order = "source" +separate_signature = true + +# -------------------------------------------------------------------------- +# Markdown extensions (Zensical defaults + what the guide relies on). +# -------------------------------------------------------------------------- +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.magiclink] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]