graphique 2.1__tar.gz → 2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {graphique-2.1 → graphique-2.2}/PKG-INFO +24 -25
- {graphique-2.1 → graphique-2.2}/README.md +16 -19
- graphique-2.2/graphique/__init__.py +21 -0
- graphique-2.2/graphique/ducklake.py +41 -0
- {graphique-2.1 → graphique-2.2}/graphique/inputs.py +23 -65
- {graphique-2.1 → graphique-2.2}/graphique/interface.py +169 -136
- {graphique-2.1 → graphique-2.2}/graphique/middleware.py +29 -27
- {graphique-2.1 → graphique-2.2}/graphique/models.py +17 -18
- graphique-2.2/graphique/partition.py +62 -0
- {graphique-2.1 → graphique-2.2}/graphique/scalars.py +19 -1
- graphique-2.2/graphique/service.py +38 -0
- {graphique-2.1 → graphique-2.2}/graphique.egg-info/PKG-INFO +24 -25
- {graphique-2.1 → graphique-2.2}/graphique.egg-info/SOURCES.txt +1 -1
- {graphique-2.1 → graphique-2.2}/graphique.egg-info/requires.txt +4 -2
- {graphique-2.1 → graphique-2.2}/pyproject.toml +15 -11
- {graphique-2.1 → graphique-2.2}/tests/test_core.py +13 -33
- graphique-2.2/tests/test_dataset.py +177 -0
- {graphique-2.1 → graphique-2.2}/tests/test_models.py +1 -1
- {graphique-2.1 → graphique-2.2}/tests/test_service.py +46 -17
- graphique-2.1/graphique/__init__.py +0 -2
- graphique-2.1/graphique/core.py +0 -100
- graphique-2.1/graphique/partition.py +0 -81
- graphique-2.1/graphique/service.py +0 -35
- graphique-2.1/tests/test_dataset.py +0 -218
- {graphique-2.1 → graphique-2.2}/LICENSE.txt +0 -0
- {graphique-2.1 → graphique-2.2}/graphique/py.typed +0 -0
- {graphique-2.1 → graphique-2.2}/graphique.egg-info/dependency_links.txt +0 -0
- {graphique-2.1 → graphique-2.2}/graphique.egg-info/top_level.txt +0 -0
- {graphique-2.1 → graphique-2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphique
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: GraphQL service for python dataframes and parquet datasets.
|
|
5
5
|
Author: A. Coady
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -8,33 +8,35 @@ Project-URL: Homepage, https://github.com/coady/graphique
|
|
|
8
8
|
Project-URL: Documentation, https://coady.github.io/graphique
|
|
9
9
|
Project-URL: Changelog, https://github.com/coady/graphique/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Issues, https://github.com/coady/graphique/issues
|
|
11
|
-
Keywords: graphql,ibis,dataframes,arrow,parquet
|
|
11
|
+
Keywords: graphql,ibis,dataframes,arrow,parquet,duckdb
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
21
|
Classifier: Topic :: Database :: Database Engines/Servers
|
|
22
22
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE.txt
|
|
29
29
|
Requires-Dist: pyarrow>=23
|
|
30
|
-
Requires-Dist: strawberry-graphql[asgi,cli]>=0.
|
|
30
|
+
Requires-Dist: strawberry-graphql[asgi,cli]>=0.328.3
|
|
31
31
|
Requires-Dist: ibis-framework>=10.7
|
|
32
|
-
Requires-Dist: pyarrow-hotfix
|
|
33
32
|
Requires-Dist: isodate>=0.7
|
|
33
|
+
Requires-Dist: pyarrow-hotfix
|
|
34
|
+
Requires-Dist: sqlglot<30.18
|
|
34
35
|
Provides-Extra: server
|
|
35
36
|
Requires-Dist: uvicorn[standard]; extra == "server"
|
|
36
37
|
Requires-Dist: duckdb; extra == "server"
|
|
37
38
|
Provides-Extra: cli
|
|
39
|
+
Requires-Dist: duckdb; extra == "cli"
|
|
38
40
|
Requires-Dist: tqdm; extra == "cli"
|
|
39
41
|
Requires-Dist: typer; extra == "cli"
|
|
40
42
|
Dynamic: license-file
|
|
@@ -49,14 +51,9 @@ Dynamic: license-file
|
|
|
49
51
|
[](https://github.com/astral-sh/ruff)
|
|
50
52
|
[](https://github.com/astral-sh/ty)
|
|
51
53
|
|
|
52
|
-
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org)
|
|
53
|
-
|
|
54
|
-
## Version 2
|
|
55
|
-
When this project started, there was no out-of-core execution engine with performance comparable to [PyArrow](https://arrow.apache.org/docs/python/index.html). So it effectively included one, based on datasets and [Acero](https://arrow.apache.org/docs/python/api/acero.html).
|
|
56
|
-
|
|
57
|
-
Since then the ecosystem has grown considerably: [DuckDB](https://duckdb.org), [DataFusion](https://datafusion.apache.org), and [Ibis](https://ibis-project.org). As of version 2, graphique is based on `ibis`. It provides a common dataframe API for multiple backends, enabling graphique to also have a default but configurable backend.
|
|
54
|
+
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) tables. Ibis supports [20+ backends](https://ibis-project.org/why#how-does-ibis-work) — DuckDB, PostgreSQL, Polars, BigQuery, etc. — so the same query API works across local files and remote databases. The schema is derived automatically.
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
[Parquet](https://parquet.apache.org) files load with `ibis.read_parquet`. For hive-partitioned data, [DuckLake](https://ducklake.select/) (`graphique.ducklake.register` / `attach`) keeps partition metadata in a catalog so DuckDB can prune and count without scanning footers. Passing a PyArrow `Dataset` as a root is deprecated.
|
|
60
57
|
|
|
61
58
|
## Usage
|
|
62
59
|
There is an example app which reads a parquet dataset.
|
|
@@ -81,20 +78,23 @@ The example app uses [Starlette's config](https://www.starlette.io/config/): in
|
|
|
81
78
|
Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
|
|
82
79
|
|
|
83
80
|
### App
|
|
84
|
-
For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables)
|
|
85
|
-
Use a `Query` type with
|
|
81
|
+
For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables).
|
|
82
|
+
Use a `Query` type with table attributes for multiple roots, and to enable federation.
|
|
86
83
|
|
|
87
84
|
```python
|
|
88
85
|
import ibis
|
|
89
86
|
from graphique import GraphQL, typed
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
app = GraphQL(
|
|
88
|
+
# any ibis backend: DuckDB, PostgreSQL, Polars, BigQuery, ...
|
|
89
|
+
table = ibis.read_parquet(...) # or `ibis.connect(...).table(...)`
|
|
90
|
+
app = GraphQL(table) # Table is root query type
|
|
91
|
+
|
|
94
92
|
|
|
95
93
|
# multiple named fields, with optional federation keys
|
|
96
94
|
class Query:
|
|
97
|
-
name =
|
|
95
|
+
name = table # or `typed(table, name, keys=...)`
|
|
96
|
+
|
|
97
|
+
|
|
98
98
|
app = GraphQL(Query)
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -106,7 +106,7 @@ uvicorn <module>:app
|
|
|
106
106
|
|
|
107
107
|
### API
|
|
108
108
|
#### types
|
|
109
|
-
* `Dataset`: interface
|
|
109
|
+
* `Dataset`: GraphQL interface over an ibis table.
|
|
110
110
|
* `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
|
|
111
111
|
* `Column`: interface for an ibis column. Each data type has a corresponding column implementation: Boolean, Int, BigInt, Float, Decimal, Date, Datetime, Time, Duration, Base64, String, Array, Struct. All columns have a `values` field for their list of scalars. Additional fields vary by type.
|
|
112
112
|
* `Row`: scalar fields. Tables are column-oriented, and graphique encourages that usage for performance. A single `row` field is provided for convenience, but a field for a list of rows is not. Requesting parallel columns is far more efficient.
|
|
@@ -131,24 +131,23 @@ uvicorn <module>:app
|
|
|
131
131
|
#### aggregation
|
|
132
132
|
* `group`: group by given columns, and aggregate the others
|
|
133
133
|
* `distinct`: group with all columns
|
|
134
|
-
* `runs`:
|
|
134
|
+
* `runs`: group by adjacency
|
|
135
135
|
* `unnest`: unnest an array column
|
|
136
136
|
* `count`, `any`: number of rows
|
|
137
137
|
|
|
138
138
|
#### ordering
|
|
139
139
|
* `order`: sort table by given columns
|
|
140
|
-
* `first`:
|
|
140
|
+
* `first`: sort and filter by rank
|
|
141
141
|
|
|
142
142
|
#### reflection
|
|
143
|
-
* `type`: type of data source
|
|
144
143
|
* `schema`: field names and types
|
|
145
144
|
* `optional`: nullable for errors
|
|
146
145
|
* `toSql`: compiles SQL query
|
|
147
146
|
|
|
148
147
|
### Performance
|
|
149
|
-
Performance is dependent on the
|
|
148
|
+
Performance is dependent on the Ibis backend, which defaults to DuckDB. There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
|
|
150
149
|
|
|
151
|
-
[
|
|
150
|
+
For partitioned parquet datasets, [DuckLake](https://ducklake.select) provides performance benefits over `ibis.read_parquet`; see `graphique.ducklake.{register,attach}` for helpers. `python -m graphique.partition` in `graphique[cli]` writes hive-layout parquet out-of-core.
|
|
152
151
|
|
|
153
152
|
## Installation
|
|
154
153
|
```console
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
|
9
9
|
[](https://github.com/astral-sh/ty)
|
|
10
10
|
|
|
11
|
-
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org)
|
|
11
|
+
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) tables. Ibis supports [20+ backends](https://ibis-project.org/why#how-does-ibis-work) — DuckDB, PostgreSQL, Polars, BigQuery, etc. — so the same query API works across local files and remote databases. The schema is derived automatically.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
When this project started, there was no out-of-core execution engine with performance comparable to [PyArrow](https://arrow.apache.org/docs/python/index.html). So it effectively included one, based on datasets and [Acero](https://arrow.apache.org/docs/python/api/acero.html).
|
|
15
|
-
|
|
16
|
-
Since then the ecosystem has grown considerably: [DuckDB](https://duckdb.org), [DataFusion](https://datafusion.apache.org), and [Ibis](https://ibis-project.org). As of version 2, graphique is based on `ibis`. It provides a common dataframe API for multiple backends, enabling graphique to also have a default but configurable backend.
|
|
17
|
-
|
|
18
|
-
Being a major version upgrade, there are incompatible changes from version 1. However the overall API remains largely the same.
|
|
13
|
+
[Parquet](https://parquet.apache.org) files load with `ibis.read_parquet`. For hive-partitioned data, [DuckLake](https://ducklake.select/) (`graphique.ducklake.register` / `attach`) keeps partition metadata in a catalog so DuckDB can prune and count without scanning footers. Passing a PyArrow `Dataset` as a root is deprecated.
|
|
19
14
|
|
|
20
15
|
## Usage
|
|
21
16
|
There is an example app which reads a parquet dataset.
|
|
@@ -40,20 +35,23 @@ The example app uses [Starlette's config](https://www.starlette.io/config/): in
|
|
|
40
35
|
Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
|
|
41
36
|
|
|
42
37
|
### App
|
|
43
|
-
For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables)
|
|
44
|
-
Use a `Query` type with
|
|
38
|
+
For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables).
|
|
39
|
+
Use a `Query` type with table attributes for multiple roots, and to enable federation.
|
|
45
40
|
|
|
46
41
|
```python
|
|
47
42
|
import ibis
|
|
48
43
|
from graphique import GraphQL, typed
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
app = GraphQL(
|
|
45
|
+
# any ibis backend: DuckDB, PostgreSQL, Polars, BigQuery, ...
|
|
46
|
+
table = ibis.read_parquet(...) # or `ibis.connect(...).table(...)`
|
|
47
|
+
app = GraphQL(table) # Table is root query type
|
|
48
|
+
|
|
53
49
|
|
|
54
50
|
# multiple named fields, with optional federation keys
|
|
55
51
|
class Query:
|
|
56
|
-
name =
|
|
52
|
+
name = table # or `typed(table, name, keys=...)`
|
|
53
|
+
|
|
54
|
+
|
|
57
55
|
app = GraphQL(Query)
|
|
58
56
|
```
|
|
59
57
|
|
|
@@ -65,7 +63,7 @@ uvicorn <module>:app
|
|
|
65
63
|
|
|
66
64
|
### API
|
|
67
65
|
#### types
|
|
68
|
-
* `Dataset`: interface
|
|
66
|
+
* `Dataset`: GraphQL interface over an ibis table.
|
|
69
67
|
* `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
|
|
70
68
|
* `Column`: interface for an ibis column. Each data type has a corresponding column implementation: Boolean, Int, BigInt, Float, Decimal, Date, Datetime, Time, Duration, Base64, String, Array, Struct. All columns have a `values` field for their list of scalars. Additional fields vary by type.
|
|
71
69
|
* `Row`: scalar fields. Tables are column-oriented, and graphique encourages that usage for performance. A single `row` field is provided for convenience, but a field for a list of rows is not. Requesting parallel columns is far more efficient.
|
|
@@ -90,24 +88,23 @@ uvicorn <module>:app
|
|
|
90
88
|
#### aggregation
|
|
91
89
|
* `group`: group by given columns, and aggregate the others
|
|
92
90
|
* `distinct`: group with all columns
|
|
93
|
-
* `runs`:
|
|
91
|
+
* `runs`: group by adjacency
|
|
94
92
|
* `unnest`: unnest an array column
|
|
95
93
|
* `count`, `any`: number of rows
|
|
96
94
|
|
|
97
95
|
#### ordering
|
|
98
96
|
* `order`: sort table by given columns
|
|
99
|
-
* `first`:
|
|
97
|
+
* `first`: sort and filter by rank
|
|
100
98
|
|
|
101
99
|
#### reflection
|
|
102
|
-
* `type`: type of data source
|
|
103
100
|
* `schema`: field names and types
|
|
104
101
|
* `optional`: nullable for errors
|
|
105
102
|
* `toSql`: compiles SQL query
|
|
106
103
|
|
|
107
104
|
### Performance
|
|
108
|
-
Performance is dependent on the
|
|
105
|
+
Performance is dependent on the Ibis backend, which defaults to DuckDB. There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
|
|
109
106
|
|
|
110
|
-
[
|
|
107
|
+
For partitioned parquet datasets, [DuckLake](https://ducklake.select) provides performance benefits over `ibis.read_parquet`; see `graphique.ducklake.{register,attach}` for helpers. `python -m graphique.partition` in `graphique[cli]` writes hive-layout parquet out-of-core.
|
|
111
108
|
|
|
112
109
|
## Installation
|
|
113
110
|
```console
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .interface import Dataset
|
|
2
|
+
from .middleware import GraphQL, MetricsExtension, implement, typed
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def fetch_record_batch(self, rows_per_batch):
|
|
6
|
+
return self.to_arrow_reader(rows_per_batch)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def connect(database=":memory:", read_only=False, config={}):
|
|
10
|
+
return original(database, read_only, {"lambda_syntax": "ENABLE_SINGLE_ARROW", **config})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import duckdb
|
|
15
|
+
except ImportError: # pragma: no cover
|
|
16
|
+
pass
|
|
17
|
+
else: # ibis-project/ibis#12196
|
|
18
|
+
if not hasattr(duckdb.DuckDBPyConnection, "fetch_arrow_table"): # pragma: no branch
|
|
19
|
+
duckdb.DuckDBPyConnection.fetch_arrow_table = duckdb.DuckDBPyConnection.to_arrow_table
|
|
20
|
+
duckdb.DuckDBPyConnection.fetch_record_batch = fetch_record_batch
|
|
21
|
+
original, duckdb.connect = duckdb.connect, connect # type: ignore
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""DuckLake helpers."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
import ibis
|
|
7
|
+
import pyarrow.dataset as ds
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@functools.singledispatch
|
|
11
|
+
def register(
|
|
12
|
+
source: str | Sequence[str],
|
|
13
|
+
table: str,
|
|
14
|
+
*partitioning: str,
|
|
15
|
+
catalog: str = ":memory:",
|
|
16
|
+
database: str = "lake.main",
|
|
17
|
+
) -> ibis.Table:
|
|
18
|
+
"""Attach a DuckLake catalog and register a parquet dataset."""
|
|
19
|
+
alias, _, schema = database.partition(".")
|
|
20
|
+
con = ibis.duckdb.connect(extensions=["ducklake"])
|
|
21
|
+
con.raw_sql(f"ATTACH 'ducklake:{catalog}' AS {alias}")
|
|
22
|
+
con.create_table(table, schema=con.read_parquet(source).schema(), database=database)
|
|
23
|
+
if columns := ", ".join(partitioning):
|
|
24
|
+
con.raw_sql(f"ALTER TABLE {database}.{table} SET PARTITIONED BY ({columns})")
|
|
25
|
+
parameters = alias, table, source, schema
|
|
26
|
+
con.raw_sql("CALL ducklake_add_data_files(?, ?, ?, schema => ?)", parameters=parameters)
|
|
27
|
+
return con.table(table, database=database)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register.register
|
|
31
|
+
def _(source: ds.Dataset, table: str, filter: ds.Expression | None = None, **kwargs) -> ibis.Table:
|
|
32
|
+
paths = [frag.path for frag in source.get_fragments(filter)]
|
|
33
|
+
return register(paths, table, *source.partitioning.schema.names, **kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def attach(table: str, catalog: str, database: str = "lake.main") -> ibis.Table:
|
|
37
|
+
"""Attach a DuckLake catalog read-only and return a table."""
|
|
38
|
+
alias, _, _ = database.partition(".")
|
|
39
|
+
con = ibis.duckdb.connect(extensions=["ducklake"])
|
|
40
|
+
con.raw_sql(f"ATTACH 'ducklake:{catalog}' AS {alias} (READ_ONLY)")
|
|
41
|
+
return con.table(table, database=database)
|
|
@@ -4,18 +4,14 @@ GraphQL input types.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import functools
|
|
8
7
|
import inspect
|
|
9
8
|
import operator
|
|
10
9
|
from collections.abc import Callable, Iterable, Iterator
|
|
11
10
|
from datetime import date, datetime, time
|
|
12
11
|
from decimal import Decimal
|
|
13
|
-
from typing import
|
|
12
|
+
from typing import TypeVar
|
|
14
13
|
|
|
15
14
|
import ibis.expr.types
|
|
16
|
-
import pyarrow as pa
|
|
17
|
-
import pyarrow.compute as pc
|
|
18
|
-
import pyarrow.dataset as ds
|
|
19
15
|
import strawberry
|
|
20
16
|
from strawberry import UNSET
|
|
21
17
|
from strawberry.annotation import StrawberryAnnotation
|
|
@@ -23,10 +19,9 @@ from strawberry.scalars import JSON
|
|
|
23
19
|
from strawberry.schema_directive import Location
|
|
24
20
|
from strawberry.types.arguments import StrawberryArgument
|
|
25
21
|
|
|
26
|
-
from .
|
|
27
|
-
from .scalars import Duration
|
|
22
|
+
from .scalars import Duration, getitems, links, order_key
|
|
28
23
|
|
|
29
|
-
T = TypeVar("T")
|
|
24
|
+
T = TypeVar("T") # needed for strawberry annotation evaluation
|
|
30
25
|
|
|
31
26
|
|
|
32
27
|
def use_doc(decorator: Callable, **kwargs):
|
|
@@ -43,10 +38,7 @@ class optional:
|
|
|
43
38
|
"""
|
|
44
39
|
|
|
45
40
|
|
|
46
|
-
@use_doc(
|
|
47
|
-
strawberry.schema_directive,
|
|
48
|
-
locations=[Location.ARGUMENT_DEFINITION, Location.FIELD_DEFINITION, Location.OBJECT],
|
|
49
|
-
)
|
|
41
|
+
@use_doc(strawberry.schema_directive, locations=[Location.OBJECT])
|
|
50
42
|
class provisional:
|
|
51
43
|
"""Provisional feature; subject to change in the future."""
|
|
52
44
|
|
|
@@ -67,7 +59,7 @@ class Field:
|
|
|
67
59
|
|
|
68
60
|
|
|
69
61
|
@strawberry.input(description="predicates for scalars")
|
|
70
|
-
class Filter
|
|
62
|
+
class Filter[T]:
|
|
71
63
|
eq: list[T] | None = default_field(description="== or `isin`", nullable=True)
|
|
72
64
|
ne: list[T] | None = default_field(description="!= or `notin`", nullable=True)
|
|
73
65
|
lt: T | None = default_field(description="<")
|
|
@@ -76,7 +68,7 @@ class Filter(Generic[T]):
|
|
|
76
68
|
ge: T | None = default_field(description=r"\>=")
|
|
77
69
|
|
|
78
70
|
def __iter__(self) -> Iterator[tuple]:
|
|
79
|
-
for name, value in self.
|
|
71
|
+
for name, value in vars(self).items():
|
|
80
72
|
if value is not UNSET:
|
|
81
73
|
yield name, value
|
|
82
74
|
|
|
@@ -89,15 +81,15 @@ class Filter(Generic[T]):
|
|
|
89
81
|
else:
|
|
90
82
|
annotation = cls[scalar] # type: ignore
|
|
91
83
|
yield StrawberryArgument(name, name, StrawberryAnnotation(annotation), default={})
|
|
92
|
-
annotation = StrawberryAnnotation(Expression
|
|
93
|
-
yield StrawberryArgument("where", None, annotation, default=
|
|
84
|
+
annotation = StrawberryAnnotation(list[Expression])
|
|
85
|
+
yield StrawberryArgument("where", None, annotation, default=[])
|
|
94
86
|
|
|
95
87
|
@staticmethod
|
|
96
|
-
def to_exprs(**
|
|
88
|
+
def to_exprs(**predicates: Filter) -> Iterable[ibis.Deferred]:
|
|
97
89
|
"""Transform query syntax into ibis expressions."""
|
|
98
|
-
for name,
|
|
90
|
+
for name, predicate in predicates.items():
|
|
99
91
|
field = ibis._[name]
|
|
100
|
-
for op, value in
|
|
92
|
+
for op, value in predicate:
|
|
101
93
|
match value, op:
|
|
102
94
|
case list(), "eq":
|
|
103
95
|
yield field == value[0] if len(value) == 1 else field.isin(value)
|
|
@@ -108,25 +100,9 @@ class Filter(Generic[T]):
|
|
|
108
100
|
case _:
|
|
109
101
|
yield getattr(operator, op)(field, value)
|
|
110
102
|
|
|
111
|
-
@staticmethod
|
|
112
|
-
def to_arrow(**queries: Filter | dict) -> ds.Expression | None:
|
|
113
|
-
"""Transform query syntax into an arrow expression."""
|
|
114
|
-
exprs = []
|
|
115
|
-
for name, query in queries.items():
|
|
116
|
-
field = pc.field(name)
|
|
117
|
-
for op, value in dict(query).items():
|
|
118
|
-
if isinstance(value, list):
|
|
119
|
-
expr = pc.is_in(field, pa.array(value))
|
|
120
|
-
exprs.append(pc.invert(expr) if op == "ne" else expr)
|
|
121
|
-
elif op == "contains": # placeholder, will not be in partition schema
|
|
122
|
-
exprs.append(field == value)
|
|
123
|
-
else:
|
|
124
|
-
exprs.append(getattr(operator, op)(field, value))
|
|
125
|
-
return functools.reduce(operator.and_, exprs or [None])
|
|
126
|
-
|
|
127
103
|
|
|
128
104
|
@strawberry.input(description="predicates for arrays")
|
|
129
|
-
class ArrayFilter
|
|
105
|
+
class ArrayFilter[T]:
|
|
130
106
|
contains: T | None = default_field(description="array contains element")
|
|
131
107
|
|
|
132
108
|
__iter__ = Filter.__iter__
|
|
@@ -228,8 +204,8 @@ class Aggregates:
|
|
|
228
204
|
sum: list[Aggregate] = default_field([], func=ibis.expr.types.NumericColumn.sum)
|
|
229
205
|
var: list[VarAggregate] = default_field([], func=ibis.expr.types.NumericColumn.var)
|
|
230
206
|
|
|
231
|
-
def __iter__(self) ->
|
|
232
|
-
for name, aggs in self.
|
|
207
|
+
def __iter__(self) -> Iterator[tuple]:
|
|
208
|
+
for name, aggs in vars(self).items():
|
|
233
209
|
for agg in aggs:
|
|
234
210
|
yield agg.to_ibis(name)
|
|
235
211
|
|
|
@@ -244,7 +220,7 @@ class Scalars:
|
|
|
244
220
|
time: time | None = default_field(description="time scalar")
|
|
245
221
|
|
|
246
222
|
def __iter__(self):
|
|
247
|
-
for value in self.
|
|
223
|
+
for value in vars(self).values():
|
|
248
224
|
if value is not UNSET:
|
|
249
225
|
yield value
|
|
250
226
|
|
|
@@ -254,9 +230,6 @@ class Expression:
|
|
|
254
230
|
name: list[str] = default_field([], description="column name(s)")
|
|
255
231
|
value: JSON | None = default_field(description="JSON scalar", nullable=True)
|
|
256
232
|
scalar: Scalars | None = default_field(description="typed scalar")
|
|
257
|
-
row_number: None = default_field(
|
|
258
|
-
func=ibis.row_number, deprecation_reason="use `window.rowNumber`"
|
|
259
|
-
)
|
|
260
233
|
|
|
261
234
|
eq: list[Expression] = default_field([], description="==")
|
|
262
235
|
ne: list[Expression] = default_field([], description="!=")
|
|
@@ -278,21 +251,9 @@ class Expression:
|
|
|
278
251
|
truediv: list[Expression] = default_field([], name="div", description="/")
|
|
279
252
|
|
|
280
253
|
coalesce: list[Expression] = default_field([], func=ibis.Column.coalesce)
|
|
281
|
-
cume_dist: Expression | None = default_field(
|
|
282
|
-
func=ibis.Column.cume_dist, deprecation_reason="use `window.cumeDist`"
|
|
283
|
-
)
|
|
284
254
|
cummax: Expression | None = default_field(func=ibis.Column.cummax)
|
|
285
255
|
cummin: Expression | None = default_field(func=ibis.Column.cummin)
|
|
286
|
-
dense_rank: Expression | None = default_field(
|
|
287
|
-
func=ibis.Column.dense_rank, deprecation_reason="use `window.denseRank`"
|
|
288
|
-
)
|
|
289
256
|
ifelse: list[Expression] = default_field([], func=ibis.expr.types.BooleanColumn.ifelse)
|
|
290
|
-
percent_rank: Expression | None = default_field(
|
|
291
|
-
func=ibis.Column.percent_rank, deprecation_reason="use `window.percentRank`"
|
|
292
|
-
)
|
|
293
|
-
rank: Expression | None = default_field(
|
|
294
|
-
func=ibis.Column.rank, deprecation_reason="use `window.rank`"
|
|
295
|
-
)
|
|
296
257
|
|
|
297
258
|
array: Arrays | None = default_field(description="array value functions")
|
|
298
259
|
numeric: Numeric | None = default_field(description="numeric functions")
|
|
@@ -301,21 +262,19 @@ class Expression:
|
|
|
301
262
|
window: Window | None = default_field(description="window functions")
|
|
302
263
|
|
|
303
264
|
def items(self) -> Iterable[tuple]:
|
|
304
|
-
for name, value in self.
|
|
265
|
+
for name, value in vars(self).items():
|
|
305
266
|
if isinstance(value, Expression):
|
|
306
267
|
yield name, [value.to_ibis()]
|
|
307
268
|
elif isinstance(value, list) and value and isinstance(value[0], Expression):
|
|
308
269
|
yield name, map(Expression.to_ibis, value)
|
|
309
270
|
|
|
310
|
-
def __iter__(self)
|
|
271
|
+
def __iter__(self):
|
|
311
272
|
if self.name:
|
|
312
273
|
yield getitems(ibis._, *self.name)
|
|
313
274
|
if self.value is not UNSET:
|
|
314
275
|
yield self.value
|
|
315
276
|
if self.scalar:
|
|
316
277
|
yield from self.scalar
|
|
317
|
-
if self.row_number is not UNSET:
|
|
318
|
-
yield ibis.row_number()
|
|
319
278
|
for name, (expr, *args) in self.items():
|
|
320
279
|
match name:
|
|
321
280
|
case "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "inv" | "and_" | "or_" | "xor":
|
|
@@ -328,11 +287,11 @@ class Expression:
|
|
|
328
287
|
if field:
|
|
329
288
|
yield from field
|
|
330
289
|
|
|
331
|
-
def to_ibis(self) -> ibis.Deferred
|
|
290
|
+
def to_ibis(self) -> ibis.Deferred:
|
|
332
291
|
fields = list(self)
|
|
333
|
-
if len(fields)
|
|
334
|
-
|
|
335
|
-
|
|
292
|
+
if len(fields) == 1:
|
|
293
|
+
return fields[0]
|
|
294
|
+
raise ValueError(f"'Expression' must specify one key (found {len(fields)})")
|
|
336
295
|
|
|
337
296
|
|
|
338
297
|
@strawberry.input(description="an `Expression` with an optional alias")
|
|
@@ -340,10 +299,9 @@ class Projection(Expression):
|
|
|
340
299
|
alias: str = strawberry.field(default="", description="name of projected column")
|
|
341
300
|
|
|
342
301
|
def to_ibis(self) -> tuple: # type: ignore
|
|
343
|
-
name
|
|
344
|
-
if name:
|
|
302
|
+
if name := self.alias or ".".join(self.name):
|
|
345
303
|
return name, super().to_ibis()
|
|
346
|
-
raise ValueError(
|
|
304
|
+
raise ValueError("'Projection' must specify a name or alias")
|
|
347
305
|
|
|
348
306
|
|
|
349
307
|
@strawberry.input(description=f"array [expressions]({links.ref}/expression-collections)")
|