graphique 2.0.1__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.1 → graphique-2.1}/PKG-INFO +31 -21
- {graphique-2.0.1 → graphique-2.1}/README.md +24 -14
- graphique-2.1/graphique/__init__.py +2 -0
- graphique-2.1/graphique/core.py +100 -0
- {graphique-2.0.1 → graphique-2.1}/graphique/inputs.py +185 -80
- {graphique-2.0.1 → graphique-2.1}/graphique/interface.py +264 -103
- graphique-2.1/graphique/middleware.py +167 -0
- {graphique-2.0.1 → graphique-2.1}/graphique/models.py +49 -54
- graphique-2.0.1/graphique/shell.py → graphique-2.1/graphique/partition.py +16 -18
- graphique-2.1/graphique/scalars.py +111 -0
- graphique-2.1/graphique/service.py +35 -0
- {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/PKG-INFO +31 -21
- {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/SOURCES.txt +1 -1
- {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/requires.txt +3 -2
- {graphique-2.0.1 → graphique-2.1}/pyproject.toml +17 -23
- graphique-2.1/tests/test_core.py +76 -0
- graphique-2.1/tests/test_dataset.py +218 -0
- graphique-2.1/tests/test_models.py +206 -0
- graphique-2.1/tests/test_service.py +396 -0
- graphique-2.0.1/graphique/__init__.py +0 -3
- graphique-2.0.1/graphique/core.py +0 -83
- graphique-2.0.1/graphique/middleware.py +0 -122
- graphique-2.0.1/graphique/scalars.py +0 -87
- graphique-2.0.1/graphique/service.py +0 -44
- graphique-2.0.1/tests/test_core.py +0 -44
- graphique-2.0.1/tests/test_dataset.py +0 -167
- graphique-2.0.1/tests/test_models.py +0 -186
- graphique-2.0.1/tests/test_service.py +0 -342
- {graphique-2.0.1 → graphique-2.1}/LICENSE.txt +0 -0
- {graphique-2.0.1 → graphique-2.1}/graphique/py.typed +0 -0
- {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/dependency_links.txt +0 -0
- {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/top_level.txt +0 -0
- {graphique-2.0.1 → graphique-2.1}/setup.cfg +0 -0
|
@@ -1,20 +1,19 @@
|
|
|
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
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -24,11 +23,11 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
|
24
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
25
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
25
|
Classifier: Typing :: Typed
|
|
27
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.11
|
|
28
27
|
Description-Content-Type: text/markdown
|
|
29
28
|
License-File: LICENSE.txt
|
|
30
|
-
Requires-Dist: pyarrow>=
|
|
31
|
-
Requires-Dist: strawberry-graphql[asgi,cli]>=0.
|
|
29
|
+
Requires-Dist: pyarrow>=23
|
|
30
|
+
Requires-Dist: strawberry-graphql[asgi,cli]>=0.288
|
|
32
31
|
Requires-Dist: ibis-framework>=10.7
|
|
33
32
|
Requires-Dist: pyarrow-hotfix
|
|
34
33
|
Requires-Dist: isodate>=0.7
|
|
@@ -37,6 +36,7 @@ Requires-Dist: uvicorn[standard]; extra == "server"
|
|
|
37
36
|
Requires-Dist: duckdb; extra == "server"
|
|
38
37
|
Provides-Extra: cli
|
|
39
38
|
Requires-Dist: tqdm; extra == "cli"
|
|
39
|
+
Requires-Dist: typer; extra == "cli"
|
|
40
40
|
Dynamic: license-file
|
|
41
41
|
|
|
42
42
|
[](https://pypi.org/project/graphique/)
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
[](https://codecov.io/gh/coady/graphique/)
|
|
48
48
|
[](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql)
|
|
49
49
|
[](https://github.com/astral-sh/ruff)
|
|
50
|
-
[](https://github.com/astral-sh/ty)
|
|
51
51
|
|
|
52
52
|
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
|
|
53
53
|
|
|
@@ -75,25 +75,27 @@ outputs the graphql schema.
|
|
|
75
75
|
The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
|
|
76
76
|
|
|
77
77
|
* PARQUET_PATH: path to the parquet directory or file
|
|
78
|
-
*
|
|
79
|
-
* METRICS = False: include timings from apollo tracing extension
|
|
78
|
+
* NAME = '': GraphQL field on `Query`; defaults to root type
|
|
80
79
|
* COLUMNS = None: list of names, or mapping of aliases, of columns to select
|
|
81
|
-
* FILTERS = None: json `filter` query for which rows to read at startup
|
|
82
80
|
|
|
83
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.
|
|
84
82
|
|
|
85
83
|
### App
|
|
86
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).
|
|
87
|
-
|
|
85
|
+
Use a `Query` type with dataset attributes for multiple roots, and to enable federation.
|
|
88
86
|
|
|
89
87
|
```python
|
|
90
88
|
import ibis
|
|
91
|
-
from graphique import GraphQL
|
|
89
|
+
from graphique import GraphQL, typed
|
|
92
90
|
|
|
93
|
-
source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
|
|
91
|
+
source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
|
|
94
92
|
# apply initial projections or filters to `source`
|
|
95
93
|
app = GraphQL(source) # Table is root query type
|
|
96
|
-
|
|
94
|
+
|
|
95
|
+
# multiple named fields, with optional federation keys
|
|
96
|
+
class Query:
|
|
97
|
+
name = source # or `typed(source, name, keys=...)`
|
|
98
|
+
app = GraphQL(Query)
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
Start like any ASGI app.
|
|
@@ -112,7 +114,8 @@ uvicorn <module>:app
|
|
|
112
114
|
#### selection
|
|
113
115
|
* `slice`: contiguous selection of rows
|
|
114
116
|
* `filter`: select rows by predicates
|
|
115
|
-
* `join`: join tables by key columns
|
|
117
|
+
* `join`, `asofJoin`, `crossJoin`: join tables by key columns
|
|
118
|
+
* `difference`, `intersect`, `union`: set operations on tables
|
|
116
119
|
* `take`: rows by index
|
|
117
120
|
* `dropNull`: remove rows with nulls
|
|
118
121
|
|
|
@@ -122,6 +125,7 @@ uvicorn <module>:app
|
|
|
122
125
|
* `column`: access a column of any type by name
|
|
123
126
|
* `row`: provides a field for each scalar of a single row
|
|
124
127
|
* `cast`: cast column types
|
|
128
|
+
* `unpack`: project struct fields
|
|
125
129
|
* `fillNull`: fill null values
|
|
126
130
|
|
|
127
131
|
#### aggregation
|
|
@@ -129,20 +133,26 @@ uvicorn <module>:app
|
|
|
129
133
|
* `distinct`: group with all columns
|
|
130
134
|
* `runs`: provisionally group by adjacency
|
|
131
135
|
* `unnest`: unnest an array column
|
|
132
|
-
* `count`: number of rows
|
|
136
|
+
* `count`, `any`: number of rows
|
|
133
137
|
|
|
134
138
|
#### ordering
|
|
135
139
|
* `order`: sort table by given columns
|
|
136
|
-
*
|
|
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
|
|
137
147
|
|
|
138
148
|
### Performance
|
|
139
|
-
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.
|
|
140
150
|
|
|
141
|
-
[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.
|
|
142
152
|
|
|
143
153
|
## Installation
|
|
144
154
|
```console
|
|
145
|
-
pip install graphique[server]
|
|
155
|
+
pip install graphique[server,cli]
|
|
146
156
|
```
|
|
147
157
|
|
|
148
158
|
## Dependencies
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://codecov.io/gh/coady/graphique/)
|
|
7
7
|
[](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql)
|
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
|
9
|
-
[](https://github.com/astral-sh/ty)
|
|
10
10
|
|
|
11
11
|
[GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
|
|
12
12
|
|
|
@@ -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)
|