graphique 2.0.2__tar.gz → 2.1__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.0.2 → graphique-2.1}/PKG-INFO +28 -17
- {graphique-2.0.2 → graphique-2.1}/README.md +23 -13
- graphique-2.1/graphique/__init__.py +2 -0
- graphique-2.1/graphique/core.py +100 -0
- {graphique-2.0.2 → graphique-2.1}/graphique/inputs.py +138 -34
- {graphique-2.0.2 → graphique-2.1}/graphique/interface.py +248 -86
- {graphique-2.0.2 → graphique-2.1}/graphique/middleware.py +83 -41
- {graphique-2.0.2 → graphique-2.1}/graphique/models.py +29 -34
- graphique-2.0.2/graphique/shell.py → graphique-2.1/graphique/partition.py +2 -4
- {graphique-2.0.2 → graphique-2.1}/graphique/scalars.py +30 -4
- graphique-2.1/graphique/service.py +35 -0
- {graphique-2.0.2 → graphique-2.1}/graphique.egg-info/PKG-INFO +28 -17
- {graphique-2.0.2 → graphique-2.1}/graphique.egg-info/SOURCES.txt +1 -1
- {graphique-2.0.2 → graphique-2.1}/graphique.egg-info/requires.txt +2 -1
- {graphique-2.0.2 → graphique-2.1}/pyproject.toml +12 -13
- {graphique-2.0.2 → graphique-2.1}/tests/test_core.py +36 -4
- {graphique-2.0.2 → graphique-2.1}/tests/test_dataset.py +93 -42
- {graphique-2.0.2 → graphique-2.1}/tests/test_models.py +24 -4
- {graphique-2.0.2 → graphique-2.1}/tests/test_service.py +57 -3
- graphique-2.0.2/graphique/__init__.py +0 -3
- graphique-2.0.2/graphique/core.py +0 -83
- graphique-2.0.2/graphique/service.py +0 -44
- {graphique-2.0.2 → graphique-2.1}/LICENSE.txt +0 -0
- {graphique-2.0.2 → graphique-2.1}/graphique/py.typed +0 -0
- {graphique-2.0.2 → graphique-2.1}/graphique.egg-info/dependency_links.txt +0 -0
- {graphique-2.0.2 → graphique-2.1}/graphique.egg-info/top_level.txt +0 -0
- {graphique-2.0.2 → graphique-2.1}/setup.cfg +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphique
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: GraphQL service for python dataframes and parquet datasets.
|
|
5
|
-
Author
|
|
5
|
+
Author: A. Coady
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
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,arrow,parquet
|
|
11
|
+
Keywords: graphql,ibis,dataframes,arrow,parquet
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -26,7 +26,7 @@ Classifier: Typing :: Typed
|
|
|
26
26
|
Requires-Python: >=3.11
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE.txt
|
|
29
|
-
Requires-Dist: pyarrow>=
|
|
29
|
+
Requires-Dist: pyarrow>=23
|
|
30
30
|
Requires-Dist: strawberry-graphql[asgi,cli]>=0.288
|
|
31
31
|
Requires-Dist: ibis-framework>=10.7
|
|
32
32
|
Requires-Dist: pyarrow-hotfix
|
|
@@ -36,6 +36,7 @@ Requires-Dist: uvicorn[standard]; extra == "server"
|
|
|
36
36
|
Requires-Dist: duckdb; extra == "server"
|
|
37
37
|
Provides-Extra: cli
|
|
38
38
|
Requires-Dist: tqdm; extra == "cli"
|
|
39
|
+
Requires-Dist: typer; extra == "cli"
|
|
39
40
|
Dynamic: license-file
|
|
40
41
|
|
|
41
42
|
[](https://pypi.org/project/graphique/)
|
|
@@ -74,25 +75,27 @@ outputs the graphql schema.
|
|
|
74
75
|
The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
|
|
75
76
|
|
|
76
77
|
* PARQUET_PATH: path to the parquet directory or file
|
|
77
|
-
*
|
|
78
|
-
* METRICS = False: include timings from apollo tracing extension
|
|
78
|
+
* NAME = '': GraphQL field on `Query`; defaults to root type
|
|
79
79
|
* COLUMNS = None: list of names, or mapping of aliases, of columns to select
|
|
80
|
-
* FILTERS = None: json `filter` query for which rows to read at startup
|
|
81
80
|
|
|
82
81
|
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.
|
|
83
82
|
|
|
84
83
|
### App
|
|
85
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) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
|
|
86
|
-
|
|
85
|
+
Use a `Query` type with dataset attributes for multiple roots, and to enable federation.
|
|
87
86
|
|
|
88
87
|
```python
|
|
89
88
|
import ibis
|
|
90
|
-
from graphique import GraphQL
|
|
89
|
+
from graphique import GraphQL, typed
|
|
91
90
|
|
|
92
|
-
source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
|
|
91
|
+
source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
|
|
93
92
|
# apply initial projections or filters to `source`
|
|
94
93
|
app = GraphQL(source) # Table is root query type
|
|
95
|
-
|
|
94
|
+
|
|
95
|
+
# multiple named fields, with optional federation keys
|
|
96
|
+
class Query:
|
|
97
|
+
name = source # or `typed(source, name, keys=...)`
|
|
98
|
+
app = GraphQL(Query)
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
Start like any ASGI app.
|
|
@@ -111,7 +114,8 @@ uvicorn <module>:app
|
|
|
111
114
|
#### selection
|
|
112
115
|
* `slice`: contiguous selection of rows
|
|
113
116
|
* `filter`: select rows by predicates
|
|
114
|
-
* `join`: join tables by key columns
|
|
117
|
+
* `join`, `asofJoin`, `crossJoin`: join tables by key columns
|
|
118
|
+
* `difference`, `intersect`, `union`: set operations on tables
|
|
115
119
|
* `take`: rows by index
|
|
116
120
|
* `dropNull`: remove rows with nulls
|
|
117
121
|
|
|
@@ -121,6 +125,7 @@ uvicorn <module>:app
|
|
|
121
125
|
* `column`: access a column of any type by name
|
|
122
126
|
* `row`: provides a field for each scalar of a single row
|
|
123
127
|
* `cast`: cast column types
|
|
128
|
+
* `unpack`: project struct fields
|
|
124
129
|
* `fillNull`: fill null values
|
|
125
130
|
|
|
126
131
|
#### aggregation
|
|
@@ -128,20 +133,26 @@ uvicorn <module>:app
|
|
|
128
133
|
* `distinct`: group with all columns
|
|
129
134
|
* `runs`: provisionally group by adjacency
|
|
130
135
|
* `unnest`: unnest an array column
|
|
131
|
-
* `count`: number of rows
|
|
136
|
+
* `count`, `any`: number of rows
|
|
132
137
|
|
|
133
138
|
#### ordering
|
|
134
139
|
* `order`: sort table by given columns
|
|
135
|
-
*
|
|
140
|
+
* `first`: provisionally sort and filter by rank
|
|
141
|
+
|
|
142
|
+
#### reflection
|
|
143
|
+
* `type`: type of data source
|
|
144
|
+
* `schema`: field names and types
|
|
145
|
+
* `optional`: nullable for errors
|
|
146
|
+
* `toSql`: compiles SQL query
|
|
136
147
|
|
|
137
148
|
### Performance
|
|
138
|
-
Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
|
|
149
|
+
Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). 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.
|
|
139
150
|
|
|
140
|
-
[PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations
|
|
151
|
+
[PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
|
|
141
152
|
|
|
142
153
|
## Installation
|
|
143
154
|
```console
|
|
144
|
-
pip install graphique[server]
|
|
155
|
+
pip install graphique[server,cli]
|
|
145
156
|
```
|
|
146
157
|
|
|
147
158
|
## Dependencies
|
|
@@ -34,25 +34,27 @@ outputs the graphql schema.
|
|
|
34
34
|
The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
|
|
35
35
|
|
|
36
36
|
* PARQUET_PATH: path to the parquet directory or file
|
|
37
|
-
*
|
|
38
|
-
* METRICS = False: include timings from apollo tracing extension
|
|
37
|
+
* NAME = '': GraphQL field on `Query`; defaults to root type
|
|
39
38
|
* COLUMNS = None: list of names, or mapping of aliases, of columns to select
|
|
40
|
-
* FILTERS = None: json `filter` query for which rows to read at startup
|
|
41
39
|
|
|
42
40
|
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.
|
|
43
41
|
|
|
44
42
|
### App
|
|
45
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) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
|
|
46
|
-
|
|
44
|
+
Use a `Query` type with dataset attributes for multiple roots, and to enable federation.
|
|
47
45
|
|
|
48
46
|
```python
|
|
49
47
|
import ibis
|
|
50
|
-
from graphique import GraphQL
|
|
48
|
+
from graphique import GraphQL, typed
|
|
51
49
|
|
|
52
|
-
source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
|
|
50
|
+
source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
|
|
53
51
|
# apply initial projections or filters to `source`
|
|
54
52
|
app = GraphQL(source) # Table is root query type
|
|
55
|
-
|
|
53
|
+
|
|
54
|
+
# multiple named fields, with optional federation keys
|
|
55
|
+
class Query:
|
|
56
|
+
name = source # or `typed(source, name, keys=...)`
|
|
57
|
+
app = GraphQL(Query)
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
Start like any ASGI app.
|
|
@@ -71,7 +73,8 @@ uvicorn <module>:app
|
|
|
71
73
|
#### selection
|
|
72
74
|
* `slice`: contiguous selection of rows
|
|
73
75
|
* `filter`: select rows by predicates
|
|
74
|
-
* `join`: join tables by key columns
|
|
76
|
+
* `join`, `asofJoin`, `crossJoin`: join tables by key columns
|
|
77
|
+
* `difference`, `intersect`, `union`: set operations on tables
|
|
75
78
|
* `take`: rows by index
|
|
76
79
|
* `dropNull`: remove rows with nulls
|
|
77
80
|
|
|
@@ -81,6 +84,7 @@ uvicorn <module>:app
|
|
|
81
84
|
* `column`: access a column of any type by name
|
|
82
85
|
* `row`: provides a field for each scalar of a single row
|
|
83
86
|
* `cast`: cast column types
|
|
87
|
+
* `unpack`: project struct fields
|
|
84
88
|
* `fillNull`: fill null values
|
|
85
89
|
|
|
86
90
|
#### aggregation
|
|
@@ -88,20 +92,26 @@ uvicorn <module>:app
|
|
|
88
92
|
* `distinct`: group with all columns
|
|
89
93
|
* `runs`: provisionally group by adjacency
|
|
90
94
|
* `unnest`: unnest an array column
|
|
91
|
-
* `count`: number of rows
|
|
95
|
+
* `count`, `any`: number of rows
|
|
92
96
|
|
|
93
97
|
#### ordering
|
|
94
98
|
* `order`: sort table by given columns
|
|
95
|
-
*
|
|
99
|
+
* `first`: provisionally sort and filter by rank
|
|
100
|
+
|
|
101
|
+
#### reflection
|
|
102
|
+
* `type`: type of data source
|
|
103
|
+
* `schema`: field names and types
|
|
104
|
+
* `optional`: nullable for errors
|
|
105
|
+
* `toSql`: compiles SQL query
|
|
96
106
|
|
|
97
107
|
### Performance
|
|
98
|
-
Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
|
|
108
|
+
Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). 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.
|
|
99
109
|
|
|
100
|
-
[PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations
|
|
110
|
+
[PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
|
|
101
111
|
|
|
102
112
|
## Installation
|
|
103
113
|
```console
|
|
104
|
-
pip install graphique[server]
|
|
114
|
+
pip install graphique[server,cli]
|
|
105
115
|
```
|
|
106
116
|
|
|
107
117
|
## Dependencies
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core arrow utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import bisect
|
|
6
|
+
import itertools
|
|
7
|
+
|
|
8
|
+
import ibis
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
import pyarrow.dataset as ds
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class links:
|
|
14
|
+
ref = "https://ibis-project.org/reference"
|
|
15
|
+
types = f"[data type]({ref}/datatypes)"
|
|
16
|
+
schema = f"[table schema]({ref}/schemas#ibis.expr.schema.Schema)"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def getitems(obj, *keys):
|
|
20
|
+
"""Nested `getitem`."""
|
|
21
|
+
for key in keys:
|
|
22
|
+
obj = obj[key]
|
|
23
|
+
return obj
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def order_key(name: str):
|
|
27
|
+
"""Parse sort order."""
|
|
28
|
+
return (ibis.desc if name.startswith("-") else ibis.asc)(ibis._[name.lstrip("-")])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rank_over(
|
|
32
|
+
table: ibis.Table,
|
|
33
|
+
by: list[str],
|
|
34
|
+
over: list[str],
|
|
35
|
+
index: ibis.Column,
|
|
36
|
+
rank: int = 1,
|
|
37
|
+
) -> ibis.Table:
|
|
38
|
+
"""Filter rows by rank within each grouping window."""
|
|
39
|
+
order_by = list(map(order_key, by))
|
|
40
|
+
table = table.mutate(_=index.over(group_by=over, order_by=order_by))
|
|
41
|
+
return table.filter(table["_"] < rank).drop("_").order_by(*order_by)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Parquet(ds.Dataset):
|
|
45
|
+
"""Partitioned parquet dataset."""
|
|
46
|
+
|
|
47
|
+
def schema(self) -> pa.Schema:
|
|
48
|
+
"""partition schema"""
|
|
49
|
+
return self.partitioning.schema if hasattr(self, "partitioning") else pa.schema([])
|
|
50
|
+
|
|
51
|
+
def keys(self, *names) -> list:
|
|
52
|
+
"""Return prefix of matching partition keys."""
|
|
53
|
+
keys = set(Parquet.schema(self).names)
|
|
54
|
+
return list(itertools.takewhile(lambda name: name.lstrip("-") in keys, names))
|
|
55
|
+
|
|
56
|
+
def fragments(self, counts: str = "", path: str = "__path__") -> ibis.Table:
|
|
57
|
+
"""Return partition fragments as a table."""
|
|
58
|
+
parts = []
|
|
59
|
+
for frag in self.get_fragments():
|
|
60
|
+
parts.append(ds.get_partition_keys(frag.partition_expression))
|
|
61
|
+
parts[-1][path] = frag.path
|
|
62
|
+
if counts:
|
|
63
|
+
parts[-1][counts] = frag.count_rows()
|
|
64
|
+
return ibis.memtable(pa.Table.from_pylist(parts))
|
|
65
|
+
|
|
66
|
+
def filter(self, expr: ds.Expression | None) -> ds.Dataset | None:
|
|
67
|
+
"""Attempt to apply filter to partition keys."""
|
|
68
|
+
try: # raises ValueError if filter references non-partition keys
|
|
69
|
+
ds.dataset([], schema=self.partitioning.schema).scanner(filter=expr)
|
|
70
|
+
except (AttributeError, ValueError):
|
|
71
|
+
return None
|
|
72
|
+
if expr is None:
|
|
73
|
+
return self
|
|
74
|
+
paths = [frag.path for frag in self.get_fragments(expr)]
|
|
75
|
+
return ds.dataset(paths, partitioning=self.partitioning)
|
|
76
|
+
|
|
77
|
+
def to_table(self, name: str | None = None) -> ibis.Table:
|
|
78
|
+
"""Return ibis `Table` from filtered dataset."""
|
|
79
|
+
paths = [frag.path for frag in self.get_fragments()]
|
|
80
|
+
return ibis.read_parquet(paths, table_name=name)
|
|
81
|
+
|
|
82
|
+
def order(self, *names: str, limit: int | None = None) -> ds.Dataset:
|
|
83
|
+
"""Return ordered partitions of the dataset."""
|
|
84
|
+
table = Parquet.fragments(self, counts="" if limit is None else "_")
|
|
85
|
+
table = table.order_by(*map(order_key, names)).cache()
|
|
86
|
+
if limit is not None:
|
|
87
|
+
limit = bisect.bisect_left(table["_"].cumsum().to_list(), limit) + 1
|
|
88
|
+
return ds.dataset(table[:limit]["__path__"].to_list(), partitioning=self.partitioning)
|
|
89
|
+
|
|
90
|
+
def first(self, *names: str, rank: int = 1, dense: bool = False) -> ds.Dataset:
|
|
91
|
+
"""Return ordered partitions up to max rank (dense or sparse)."""
|
|
92
|
+
keys = {name.strip("-"): order_key(name) for name in names}
|
|
93
|
+
if dense or rank == 1:
|
|
94
|
+
table = Parquet.fragments(self).aggregate(_=ibis._["__path__"].collect(), by=list(keys))
|
|
95
|
+
paths = table.order_by(*keys.values())[:rank]["_"].unnest()
|
|
96
|
+
else:
|
|
97
|
+
table = Parquet.fragments(self, counts="_").order_by(*keys.values()).cache()
|
|
98
|
+
limit = bisect.bisect_left(table["_"].cumsum().to_list(), rank) + 1
|
|
99
|
+
paths = table.semi_join(table[:limit].select(*keys).distinct(), list(keys))["__path__"]
|
|
100
|
+
return ds.dataset(paths.to_list(), partitioning=self.partitioning)
|
|
@@ -12,7 +12,7 @@ from datetime import date, datetime, time
|
|
|
12
12
|
from decimal import Decimal
|
|
13
13
|
from typing import Generic, TypeVar
|
|
14
14
|
|
|
15
|
-
import ibis
|
|
15
|
+
import ibis.expr.types
|
|
16
16
|
import pyarrow as pa
|
|
17
17
|
import pyarrow.compute as pc
|
|
18
18
|
import pyarrow.dataset as ds
|
|
@@ -22,7 +22,6 @@ from strawberry.annotation import StrawberryAnnotation
|
|
|
22
22
|
from strawberry.scalars import JSON
|
|
23
23
|
from strawberry.schema_directive import Location
|
|
24
24
|
from strawberry.types.arguments import StrawberryArgument
|
|
25
|
-
from strawberry.types.field import StrawberryField
|
|
26
25
|
|
|
27
26
|
from .core import getitems, links, order_key
|
|
28
27
|
from .scalars import Duration
|
|
@@ -44,14 +43,15 @@ class optional:
|
|
|
44
43
|
"""
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
@use_doc(
|
|
46
|
+
@use_doc(
|
|
47
|
+
strawberry.schema_directive,
|
|
48
|
+
locations=[Location.ARGUMENT_DEFINITION, Location.FIELD_DEFINITION, Location.OBJECT],
|
|
49
|
+
)
|
|
48
50
|
class provisional:
|
|
49
51
|
"""Provisional feature; subject to change in the future."""
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
def default_field(
|
|
53
|
-
default=UNSET, func: Callable | None = None, nullable: bool = False, **kwargs
|
|
54
|
-
) -> StrawberryField:
|
|
54
|
+
def default_field(default=UNSET, func: Callable | None = None, nullable: bool = False, **kwargs):
|
|
55
55
|
"""Use dataclass `default_factory` for `UNSET` or mutables."""
|
|
56
56
|
if func is not None:
|
|
57
57
|
kwargs["description"] = inspect.getdoc(func).splitlines()[0]
|
|
@@ -83,10 +83,12 @@ class Filter(Generic[T]):
|
|
|
83
83
|
@classmethod
|
|
84
84
|
def resolve_args(cls, types: dict) -> Iterable[StrawberryArgument]:
|
|
85
85
|
"""Generate dynamically resolved arguments for filter field."""
|
|
86
|
-
for name in types:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
for name, scalar in types.items():
|
|
87
|
+
if getattr(scalar, "__origin__", None) is list:
|
|
88
|
+
annotation = ArrayFilter[scalar.__args__[0]]
|
|
89
|
+
else:
|
|
90
|
+
annotation = cls[scalar] # type: ignore
|
|
91
|
+
yield StrawberryArgument(name, name, StrawberryAnnotation(annotation), default={})
|
|
90
92
|
annotation = StrawberryAnnotation(Expression | None)
|
|
91
93
|
yield StrawberryArgument("where", None, annotation, default=None)
|
|
92
94
|
|
|
@@ -98,9 +100,11 @@ class Filter(Generic[T]):
|
|
|
98
100
|
for op, value in query:
|
|
99
101
|
match value, op:
|
|
100
102
|
case list(), "eq":
|
|
101
|
-
yield field.isin(value)
|
|
103
|
+
yield field == value[0] if len(value) == 1 else field.isin(value)
|
|
102
104
|
case list(), "ne":
|
|
103
|
-
yield field.notin(value)
|
|
105
|
+
yield field != value[0] if len(value) == 1 else field.notin(value)
|
|
106
|
+
case _, "contains":
|
|
107
|
+
yield field.contains(value)
|
|
104
108
|
case _:
|
|
105
109
|
yield getattr(operator, op)(field, value)
|
|
106
110
|
|
|
@@ -114,28 +118,37 @@ class Filter(Generic[T]):
|
|
|
114
118
|
if isinstance(value, list):
|
|
115
119
|
expr = pc.is_in(field, pa.array(value))
|
|
116
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)
|
|
117
123
|
else:
|
|
118
124
|
exprs.append(getattr(operator, op)(field, value))
|
|
119
125
|
return functools.reduce(operator.and_, exprs or [None])
|
|
120
126
|
|
|
121
127
|
|
|
128
|
+
@strawberry.input(description="predicates for arrays")
|
|
129
|
+
class ArrayFilter(Generic[T]):
|
|
130
|
+
contains: T | None = default_field(description="array contains element")
|
|
131
|
+
|
|
132
|
+
__iter__ = Filter.__iter__
|
|
133
|
+
|
|
134
|
+
|
|
122
135
|
@strawberry.input(description="name and optional alias for aggregation")
|
|
123
136
|
class Aggregate:
|
|
124
137
|
name: str = strawberry.field(description="column name")
|
|
125
138
|
alias: str = strawberry.field(default="", description="output column name")
|
|
126
139
|
where: Expression | None = None
|
|
127
140
|
|
|
128
|
-
def to_ibis(self, func: str, **options) -> tuple:
|
|
141
|
+
def to_ibis(self, func: str, *args, **options) -> tuple:
|
|
129
142
|
options["where"] = self.where and self.where.to_ibis()
|
|
130
|
-
return (self.alias or self.name), getattr(ibis._[self.name], func)(**options)
|
|
143
|
+
return (self.alias or self.name), getattr(ibis._[self.name], func)(*args, **options)
|
|
131
144
|
|
|
132
145
|
|
|
133
146
|
@strawberry.input
|
|
134
147
|
class UniqueAggregate(Aggregate):
|
|
135
148
|
approx: bool = False
|
|
136
149
|
|
|
137
|
-
def to_ibis(self, func: str,
|
|
138
|
-
return super().to_ibis("approx_" + func if self.approx else func,
|
|
150
|
+
def to_ibis(self, func: str, *args) -> tuple:
|
|
151
|
+
return super().to_ibis("approx_" + func if self.approx else func, *args)
|
|
139
152
|
|
|
140
153
|
|
|
141
154
|
@strawberry.input
|
|
@@ -155,7 +168,7 @@ class OrderAggregate(Aggregate):
|
|
|
155
168
|
class VarAggregate(Aggregate):
|
|
156
169
|
how: str = "sample"
|
|
157
170
|
|
|
158
|
-
def to_ibis(self, func: str
|
|
171
|
+
def to_ibis(self, func: str) -> tuple:
|
|
159
172
|
return super().to_ibis(func, how=self.how)
|
|
160
173
|
|
|
161
174
|
|
|
@@ -163,8 +176,8 @@ class VarAggregate(Aggregate):
|
|
|
163
176
|
class QuantileAggregate(UniqueAggregate):
|
|
164
177
|
q: float = 0.5
|
|
165
178
|
|
|
166
|
-
def to_ibis(self, func: str,
|
|
167
|
-
return super().to_ibis(func,
|
|
179
|
+
def to_ibis(self, func: str, *_) -> tuple:
|
|
180
|
+
return super().to_ibis(func, self.q)
|
|
168
181
|
|
|
169
182
|
|
|
170
183
|
@strawberry.input
|
|
@@ -175,17 +188,40 @@ class CollectAggregate(OrderAggregate):
|
|
|
175
188
|
return super().to_ibis(func, distinct=self.distinct)
|
|
176
189
|
|
|
177
190
|
|
|
191
|
+
@strawberry.input
|
|
192
|
+
class ConcatAggregate(Aggregate):
|
|
193
|
+
sep: str = ","
|
|
194
|
+
order_by: list[str] = default_field([])
|
|
195
|
+
|
|
196
|
+
def to_ibis(self, func: str) -> tuple:
|
|
197
|
+
order_by = list(map(order_key, self.order_by)) or None
|
|
198
|
+
return super().to_ibis("group_" + func, sep=self.sep, order_by=order_by)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@strawberry.input
|
|
202
|
+
class ArgAggregate(Aggregate):
|
|
203
|
+
key: str
|
|
204
|
+
|
|
205
|
+
def to_ibis(self, func: str) -> tuple:
|
|
206
|
+
return super().to_ibis(func, key=self.key)
|
|
207
|
+
|
|
208
|
+
|
|
178
209
|
@strawberry.input(description=f"aggregation [expressions]({links.ref}/expression-generic)")
|
|
179
210
|
class Aggregates:
|
|
180
211
|
all: list[Aggregate] = default_field([], func=ibis.expr.types.BooleanColumn.all)
|
|
181
212
|
any: list[Aggregate] = default_field([], func=ibis.expr.types.BooleanColumn.any)
|
|
213
|
+
argmax: list[ArgAggregate] = default_field([], func=ibis.Column.argmax)
|
|
214
|
+
argmin: list[ArgAggregate] = default_field([], func=ibis.Column.argmin)
|
|
182
215
|
collect: list[CollectAggregate] = default_field([], func=ibis.Column.collect)
|
|
216
|
+
concat: list[ConcatAggregate] = default_field([], func=ibis.Column.group_concat)
|
|
183
217
|
count: list[Aggregate] = default_field([], func=ibis.Column.count)
|
|
184
218
|
first: list[OrderAggregate] = default_field([], func=ibis.Column.first)
|
|
219
|
+
kurtosis: list[VarAggregate] = default_field([], func=ibis.expr.types.NumericColumn.kurtosis)
|
|
185
220
|
last: list[OrderAggregate] = default_field([], func=ibis.Column.last)
|
|
186
221
|
max: list[Aggregate] = default_field([], func=ibis.Column.max)
|
|
187
222
|
mean: list[Aggregate] = default_field([], func=ibis.expr.types.NumericColumn.mean)
|
|
188
223
|
min: list[Aggregate] = default_field([], func=ibis.Column.min)
|
|
224
|
+
mode: list[Aggregate] = default_field([], func=ibis.Column.mode)
|
|
189
225
|
nunique: list[UniqueAggregate] = default_field([], func=ibis.Column.nunique)
|
|
190
226
|
quantile: list[QuantileAggregate] = default_field([], func=ibis.Column.quantile)
|
|
191
227
|
std: list[VarAggregate] = default_field([], func=ibis.expr.types.NumericColumn.std)
|
|
@@ -198,7 +234,7 @@ class Aggregates:
|
|
|
198
234
|
yield agg.to_ibis(name)
|
|
199
235
|
|
|
200
236
|
|
|
201
|
-
@strawberry.input(description="typed scalars")
|
|
237
|
+
@strawberry.input(description="typed scalars", one_of=True)
|
|
202
238
|
class Scalars:
|
|
203
239
|
base64: strawberry.scalars.Base64 | None = default_field(description="binary scalar")
|
|
204
240
|
date: date | None = default_field(description="date scalar")
|
|
@@ -218,7 +254,9 @@ class Expression:
|
|
|
218
254
|
name: list[str] = default_field([], description="column name(s)")
|
|
219
255
|
value: JSON | None = default_field(description="JSON scalar", nullable=True)
|
|
220
256
|
scalar: Scalars | None = default_field(description="typed scalar")
|
|
221
|
-
row_number: None = default_field(
|
|
257
|
+
row_number: None = default_field(
|
|
258
|
+
func=ibis.row_number, deprecation_reason="use `window.rowNumber`"
|
|
259
|
+
)
|
|
222
260
|
|
|
223
261
|
eq: list[Expression] = default_field([], description="==")
|
|
224
262
|
ne: list[Expression] = default_field([], description="!=")
|
|
@@ -240,13 +278,21 @@ class Expression:
|
|
|
240
278
|
truediv: list[Expression] = default_field([], name="div", description="/")
|
|
241
279
|
|
|
242
280
|
coalesce: list[Expression] = default_field([], func=ibis.Column.coalesce)
|
|
243
|
-
cume_dist: Expression | None = default_field(
|
|
281
|
+
cume_dist: Expression | None = default_field(
|
|
282
|
+
func=ibis.Column.cume_dist, deprecation_reason="use `window.cumeDist`"
|
|
283
|
+
)
|
|
244
284
|
cummax: Expression | None = default_field(func=ibis.Column.cummax)
|
|
245
285
|
cummin: Expression | None = default_field(func=ibis.Column.cummin)
|
|
246
|
-
dense_rank: Expression | None = default_field(
|
|
286
|
+
dense_rank: Expression | None = default_field(
|
|
287
|
+
func=ibis.Column.dense_rank, deprecation_reason="use `window.denseRank`"
|
|
288
|
+
)
|
|
247
289
|
ifelse: list[Expression] = default_field([], func=ibis.expr.types.BooleanColumn.ifelse)
|
|
248
|
-
percent_rank: Expression | None = default_field(
|
|
249
|
-
|
|
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
|
+
)
|
|
250
296
|
|
|
251
297
|
array: Arrays | None = default_field(description="array value functions")
|
|
252
298
|
numeric: Numeric | None = default_field(description="numeric functions")
|
|
@@ -283,10 +329,10 @@ class Expression:
|
|
|
283
329
|
yield from field
|
|
284
330
|
|
|
285
331
|
def to_ibis(self) -> ibis.Deferred | None:
|
|
286
|
-
fields = list(self)
|
|
287
|
-
if len(fields)
|
|
288
|
-
|
|
289
|
-
|
|
332
|
+
fields = list(self)
|
|
333
|
+
if len(fields) > 1:
|
|
334
|
+
raise ValueError("conflicting inputs: " + ", ".join(map(str, fields)))
|
|
335
|
+
return next(iter(fields), None) # type: ignore
|
|
290
336
|
|
|
291
337
|
|
|
292
338
|
@strawberry.input(description="an `Expression` with an optional alias")
|
|
@@ -304,6 +350,7 @@ class Projection(Expression):
|
|
|
304
350
|
class Arrays:
|
|
305
351
|
alls: Expression | None = default_field(func=ibis.expr.types.ArrayValue.alls)
|
|
306
352
|
anys: Expression | None = default_field(func=ibis.expr.types.ArrayValue.anys)
|
|
353
|
+
flatten: Expression | None = default_field(func=ibis.expr.types.ArrayValue.flatten)
|
|
307
354
|
length: Expression | None = default_field(func=ibis.expr.types.ArrayValue.length)
|
|
308
355
|
maxs: Expression | None = default_field(func=ibis.expr.types.ArrayValue.maxs)
|
|
309
356
|
means: Expression | None = default_field(func=ibis.expr.types.ArrayValue.means)
|
|
@@ -313,13 +360,25 @@ class Arrays:
|
|
|
313
360
|
sums: Expression | None = default_field(func=ibis.expr.types.ArrayValue.sums)
|
|
314
361
|
unique: Expression | None = default_field(func=ibis.expr.types.ArrayValue.unique)
|
|
315
362
|
|
|
363
|
+
concat: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.concat)
|
|
364
|
+
contains: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.contains)
|
|
316
365
|
index: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.index)
|
|
366
|
+
intersect: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.intersect)
|
|
367
|
+
remove: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.remove)
|
|
368
|
+
union: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.union)
|
|
369
|
+
zip: list[Expression] = default_field([], func=ibis.expr.types.ArrayValue.zip)
|
|
317
370
|
|
|
318
371
|
slice: Expression | None = default_field(description="array slice")
|
|
319
372
|
value: Expression | None = default_field(description="value at offset")
|
|
320
373
|
offset: int = 0
|
|
321
374
|
limit: int | None = None
|
|
322
375
|
|
|
376
|
+
join: Expression | None = default_field(func=ibis.expr.types.ArrayValue.join)
|
|
377
|
+
sep: str = ""
|
|
378
|
+
|
|
379
|
+
repeat: Expression | None = default_field(func=ibis.expr.types.ArrayValue.repeat)
|
|
380
|
+
n: int = 1
|
|
381
|
+
|
|
323
382
|
def __iter__(self) -> Iterator[ibis.Deferred]:
|
|
324
383
|
for name, (expr, *args) in Expression.items(self):
|
|
325
384
|
match name:
|
|
@@ -327,6 +386,10 @@ class Arrays:
|
|
|
327
386
|
yield expr[self.offset :][: self.limit]
|
|
328
387
|
case "value":
|
|
329
388
|
yield expr[self.offset]
|
|
389
|
+
case "join":
|
|
390
|
+
yield expr.join(self.sep)
|
|
391
|
+
case "repeat":
|
|
392
|
+
yield expr.repeat(self.n)
|
|
330
393
|
case _:
|
|
331
394
|
yield getattr(expr, name)(*args)
|
|
332
395
|
|
|
@@ -448,6 +511,9 @@ class Temporal:
|
|
|
448
511
|
description=f"window [expressions]({links.ref}/expression-tables.html#ibis.window)"
|
|
449
512
|
)
|
|
450
513
|
class Window:
|
|
514
|
+
over: list[str] = default_field([], description="partition column names")
|
|
515
|
+
by: list[str] = default_field([], description="order column names within each partition")
|
|
516
|
+
|
|
451
517
|
lag: Expression | None = default_field(func=ibis.Column.lag)
|
|
452
518
|
lead: Expression | None = default_field(func=ibis.Column.lead)
|
|
453
519
|
|
|
@@ -459,15 +525,53 @@ class Window:
|
|
|
459
525
|
ge: Expression | None = default_field(description=r"pairwise \>=")
|
|
460
526
|
sub: Expression | None = default_field(description="pairwise -")
|
|
461
527
|
|
|
462
|
-
|
|
463
|
-
|
|
528
|
+
count: Expression | None = default_field(func=ibis.Column.count)
|
|
529
|
+
sum: Expression | None = default_field(func=ibis.expr.types.NumericColumn.sum)
|
|
530
|
+
mean: Expression | None = default_field(func=ibis.expr.types.NumericColumn.mean)
|
|
531
|
+
min: Expression | None = default_field(func=ibis.Column.min)
|
|
532
|
+
max: Expression | None = default_field(func=ibis.Column.max)
|
|
533
|
+
|
|
534
|
+
row_number: None = default_field(func=ibis.row_number)
|
|
535
|
+
rank: None = default_field(func=ibis.rank)
|
|
536
|
+
dense_rank: None = default_field(func=ibis.dense_rank)
|
|
537
|
+
percent_rank: None = default_field(func=ibis.percent_rank)
|
|
538
|
+
cume_dist: None = default_field(func=ibis.cume_dist)
|
|
539
|
+
ntile: int = default_field(0, func=ibis.ntile)
|
|
540
|
+
|
|
541
|
+
preceding: int | None = strawberry.field(
|
|
542
|
+
default=None, description="number of preceding rows in the window"
|
|
543
|
+
)
|
|
544
|
+
following: int | None = strawberry.field(
|
|
545
|
+
default=None, description="number of following rows in the window"
|
|
546
|
+
)
|
|
547
|
+
range: bool = strawberry.field(
|
|
548
|
+
default=False, description="aggregate rows based upon differences in the order by values"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
offset: int = strawberry.field(default=1, description="offset for lag and lead")
|
|
552
|
+
default: JSON | None = strawberry.field(default=None, description="default JSON scalar")
|
|
464
553
|
scalar: Scalars | None = default_field(description="default typed scalar")
|
|
465
554
|
|
|
466
|
-
def __iter__(self) -> Iterator[ibis.
|
|
555
|
+
def __iter__(self) -> Iterator[ibis.Expr]:
|
|
467
556
|
(default,) = self.scalar or [self.default]
|
|
557
|
+
orderings = list(map(order_key, self.by))
|
|
558
|
+
win = cumwin = None
|
|
559
|
+
if self.over or self.by:
|
|
560
|
+
frame = ibis.range_window if self.range else ibis.window
|
|
561
|
+
win = frame(self.preceding, self.following, group_by=self.over, order_by=orderings)
|
|
562
|
+
if self.by and self.preceding is self.following is None:
|
|
563
|
+
cumwin = ibis.cumulative_window(group_by=self.over, order_by=orderings)
|
|
564
|
+
for name in ("row_number", "rank", "dense_rank", "percent_rank", "cume_dist"):
|
|
565
|
+
if getattr(self, name) is not UNSET:
|
|
566
|
+
yield getattr(ibis, name)().over(win)
|
|
567
|
+
if self.ntile:
|
|
568
|
+
yield ibis.ntile(self.ntile).over(win).cast("int32")
|
|
468
569
|
for name, (expr,) in Expression.items(self):
|
|
469
570
|
match name:
|
|
571
|
+
case "count" | "sum" | "mean" | "min" | "max":
|
|
572
|
+
yield getattr(expr, name)().over(cumwin or win)
|
|
470
573
|
case "lag" | "lead":
|
|
471
|
-
yield getattr(expr, name)(self.offset, default)
|
|
574
|
+
yield getattr(expr, name)(self.offset, default).over(win)
|
|
472
575
|
case _:
|
|
473
|
-
|
|
576
|
+
lag = expr.lag(self.offset).over(win)
|
|
577
|
+
yield getattr(operator, name)(expr, lag).fill_null(default)
|