graphique 1.4__tar.gz → 1.5__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-1.4 → graphique-1.5}/PKG-INFO +2 -3
- {graphique-1.4 → graphique-1.5}/README.md +0 -1
- {graphique-1.4 → graphique-1.5}/graphique/__init__.py +1 -1
- {graphique-1.4 → graphique-1.5}/graphique/core.py +9 -5
- {graphique-1.4 → graphique-1.5}/graphique/inputs.py +1 -0
- {graphique-1.4 → graphique-1.5}/graphique/interface.py +11 -8
- {graphique-1.4 → graphique-1.5}/graphique/middleware.py +3 -1
- {graphique-1.4 → graphique-1.5}/graphique/models.py +3 -3
- {graphique-1.4 → graphique-1.5}/graphique/scalars.py +1 -0
- {graphique-1.4 → graphique-1.5}/graphique/service.py +2 -1
- {graphique-1.4 → graphique-1.5}/graphique/shell.py +23 -12
- {graphique-1.4 → graphique-1.5}/graphique.egg-info/PKG-INFO +2 -3
- {graphique-1.4 → graphique-1.5}/graphique.egg-info/SOURCES.txt +1 -0
- {graphique-1.4 → graphique-1.5}/graphique.egg-info/requires.txt +1 -1
- {graphique-1.4 → graphique-1.5}/pyproject.toml +7 -6
- graphique-1.5/requirements.in +2 -0
- {graphique-1.4 → graphique-1.5}/tests/test_dataset.py +38 -65
- {graphique-1.4 → graphique-1.5}/tests/test_models.py +76 -134
- {graphique-1.4 → graphique-1.5}/tests/test_service.py +91 -152
- {graphique-1.4 → graphique-1.5}/LICENSE.txt +0 -0
- {graphique-1.4 → graphique-1.5}/graphique/py.typed +0 -0
- {graphique-1.4 → graphique-1.5}/graphique.egg-info/dependency_links.txt +0 -0
- {graphique-1.4 → graphique-1.5}/graphique.egg-info/top_level.txt +0 -0
- {graphique-1.4 → graphique-1.5}/setup.cfg +0 -0
- {graphique-1.4 → graphique-1.5}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: graphique
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5
|
|
4
4
|
Summary: GraphQL service for arrow tables and parquet data sets.
|
|
5
5
|
Author-email: Aric Coady <aric.coady@gmail.com>
|
|
6
6
|
License: Copyright 2022 Aric Coady
|
|
@@ -40,7 +40,7 @@ Classifier: Typing :: Typed
|
|
|
40
40
|
Requires-Python: >=3.9
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
License-File: LICENSE.txt
|
|
43
|
-
Requires-Dist: pyarrow>=
|
|
43
|
+
Requires-Dist: pyarrow>=15
|
|
44
44
|
Requires-Dist: strawberry-graphql[asgi,cli]>=0.198
|
|
45
45
|
Provides-Extra: server
|
|
46
46
|
Requires-Dist: uvicorn[standard]; extra == "server"
|
|
@@ -54,7 +54,6 @@ Requires-Dist: tqdm; extra == "cli"
|
|
|
54
54
|
[](https://github.com/coady/graphique/actions)
|
|
55
55
|
[](https://codecov.io/gh/coady/graphique/)
|
|
56
56
|
[](https://github.com/coady/graphique/security/code-scanning)
|
|
57
|
-
[](https://pypi.org/project/black/)
|
|
58
57
|
[](https://github.com/astral-sh/ruff)
|
|
59
58
|
[](https://mypy-lang.org/)
|
|
60
59
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
[](https://github.com/coady/graphique/actions)
|
|
6
6
|
[](https://codecov.io/gh/coady/graphique/)
|
|
7
7
|
[](https://github.com/coady/graphique/security/code-scanning)
|
|
8
|
-
[](https://pypi.org/project/black/)
|
|
9
8
|
[](https://github.com/astral-sh/ruff)
|
|
10
9
|
[](https://mypy-lang.org/)
|
|
11
10
|
|
|
@@ -4,6 +4,7 @@ Core utilities that add pandas-esque features to arrow arrays and tables.
|
|
|
4
4
|
Arrow forbids subclassing, so the classes are for logical grouping.
|
|
5
5
|
Their methods are called as functions.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import bisect
|
|
8
9
|
import contextlib
|
|
9
10
|
import functools
|
|
@@ -106,7 +107,10 @@ def register(func: Callable, kind: str = 'scalar') -> pc.Function:
|
|
|
106
107
|
|
|
107
108
|
@register
|
|
108
109
|
def digitize(
|
|
109
|
-
ctx,
|
|
110
|
+
ctx,
|
|
111
|
+
array: pa.float64(), # type: ignore
|
|
112
|
+
bins: pa.list_(pa.float64()), # type: ignore
|
|
113
|
+
right: pa.bool_(), # type: ignore
|
|
110
114
|
) -> pa.int64(): # type: ignore
|
|
111
115
|
"""Return the indices of the bins to which each value in input array belongs."""
|
|
112
116
|
return pa.array(np.digitize(array, bins.values, right.as_py()))
|
|
@@ -347,7 +351,7 @@ class Table(pa.Table):
|
|
|
347
351
|
"""Return table with union of columns."""
|
|
348
352
|
columns: dict = {}
|
|
349
353
|
for table in tables:
|
|
350
|
-
columns
|
|
354
|
+
columns |= Table.columns(table)
|
|
351
355
|
return type(tables[0]).from_pydict(columns)
|
|
352
356
|
|
|
353
357
|
def range(self, name: str, lower=None, upper=None, **includes) -> pa.Table:
|
|
@@ -428,10 +432,10 @@ class Table(pa.Table):
|
|
|
428
432
|
row = {counts: len(self)} if counts else {}
|
|
429
433
|
for key in ('one', 'list', 'distinct'): # hash only functions
|
|
430
434
|
func, aggs = getattr(Agg, key), funcs.pop(key, [])
|
|
431
|
-
row
|
|
435
|
+
row |= {agg.alias: func(self[agg.name], **agg.options) for agg in aggs}
|
|
432
436
|
if funcs:
|
|
433
437
|
table = Table.group(self, **funcs) # type: ignore
|
|
434
|
-
row
|
|
438
|
+
row |= {name: table[name][0] for name in table.schema.names}
|
|
435
439
|
for name, value in row.items():
|
|
436
440
|
if isinstance(value, pa.ChunkedArray):
|
|
437
441
|
row[name] = value.combine_chunks()
|
|
@@ -614,7 +618,7 @@ class Table(pa.Table):
|
|
|
614
618
|
yield None
|
|
615
619
|
else:
|
|
616
620
|
row = {name: pa.repeat(self[name][index], count) for name in scalars}
|
|
617
|
-
row
|
|
621
|
+
row |= {name: self[name][index].values for name in lists}
|
|
618
622
|
yield pa.RecordBatch.from_pydict(row)
|
|
619
623
|
|
|
620
624
|
def size(self) -> str:
|
|
@@ -3,6 +3,7 @@ Primary Dataset interface.
|
|
|
3
3
|
|
|
4
4
|
Doesn't require knowledge of the schema.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
# mypy: disable-error-code=valid-type
|
|
7
8
|
import collections
|
|
8
9
|
import inspect
|
|
@@ -234,7 +235,11 @@ class Dataset:
|
|
|
234
235
|
aggregate="aggregation functions applied to other columns",
|
|
235
236
|
)
|
|
236
237
|
def group(
|
|
237
|
-
self,
|
|
238
|
+
self,
|
|
239
|
+
info: Info,
|
|
240
|
+
by: list[str] = [],
|
|
241
|
+
counts: str = '',
|
|
242
|
+
aggregate: HashAggregates = {}, # type: ignore
|
|
238
243
|
) -> Self:
|
|
239
244
|
"""Return table grouped by columns.
|
|
240
245
|
|
|
@@ -277,11 +282,11 @@ class Dataset:
|
|
|
277
282
|
row = ds.get_partition_keys(fragment.partition_expression)
|
|
278
283
|
if projection:
|
|
279
284
|
table = fragment.to_table(columns=projection)
|
|
280
|
-
row
|
|
285
|
+
row |= T.aggregate(table, counts=counts, **aggs)
|
|
281
286
|
elif counts:
|
|
282
287
|
row[counts] = fragment.count_rows()
|
|
283
288
|
arrays = {name: value for name, value in row.items() if isinstance(value, pa.Array)}
|
|
284
|
-
row
|
|
289
|
+
row |= T.columns(pa.RecordBatch.from_pydict(arrays))
|
|
285
290
|
for name in row:
|
|
286
291
|
columns[name].append(row[name])
|
|
287
292
|
for name, values in columns.items():
|
|
@@ -289,7 +294,7 @@ class Dataset:
|
|
|
289
294
|
columns[name] = C.from_scalars(values)
|
|
290
295
|
elif isinstance(values[0], pa.Array):
|
|
291
296
|
columns[name] = ListChunk.from_scalars(values)
|
|
292
|
-
columns
|
|
297
|
+
columns |= {field.name: pa.array(columns[field.name], field.type) for field in schema}
|
|
293
298
|
return self.add_metric(info, pa.table(columns), mode='fragment')
|
|
294
299
|
|
|
295
300
|
@doc_field(
|
|
@@ -482,7 +487,7 @@ class Dataset:
|
|
|
482
487
|
def project(self, info: Info, columns: list[Projection]) -> dict:
|
|
483
488
|
"""Return projected columns, including all references from below fields."""
|
|
484
489
|
projection = {name: pc.field(name) for name in self.references(info, level=1)}
|
|
485
|
-
projection
|
|
490
|
+
projection |= {col.alias or '.'.join(col.name): col.to_arrow() for col in columns}
|
|
486
491
|
if '' in projection:
|
|
487
492
|
raise ValueError(f"projected columns need a name or alias: {projection['']}")
|
|
488
493
|
return projection
|
|
@@ -497,9 +502,7 @@ class Dataset:
|
|
|
497
502
|
return scanner
|
|
498
503
|
|
|
499
504
|
@doc_field(filter="selected rows", columns="projected columns")
|
|
500
|
-
def scan(
|
|
501
|
-
self, info: Info, filter: Expression = {}, columns: list[Projection] = [] # type: ignore
|
|
502
|
-
) -> Self:
|
|
505
|
+
def scan(self, info: Info, filter: Expression = {}, columns: list[Projection] = []) -> Self: # type: ignore
|
|
503
506
|
"""Select rows and project columns without memory usage."""
|
|
504
507
|
expr = filter.to_arrow()
|
|
505
508
|
if expr is not None and not columns and isinstance(self.table, ds.Dataset):
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ASGI GraphQL utilities.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import warnings
|
|
5
6
|
from collections.abc import Iterable, Mapping
|
|
6
7
|
from datetime import timedelta
|
|
8
|
+
from keyword import iskeyword
|
|
7
9
|
from typing import Optional
|
|
8
10
|
import pyarrow.dataset as ds
|
|
9
11
|
import strawberry.asgi
|
|
@@ -94,7 +96,7 @@ def implemented(root: Root, name: str = '', keys: Iterable = ()):
|
|
|
94
96
|
"""Return type which extends the Dataset interface with knowledge of the schema."""
|
|
95
97
|
schema = root.projected_schema if isinstance(root, ds.Scanner) else root.schema
|
|
96
98
|
types = {field.name: type_map[C.scalar_type(field).id] for field in schema}
|
|
97
|
-
types = {name: types[name] for name in types if name.isidentifier()}
|
|
99
|
+
types = {name: types[name] for name in types if name.isidentifier() and not iskeyword(name)}
|
|
98
100
|
if invalid := set(schema.names) - set(types):
|
|
99
101
|
warnings.warn(f'invalid field names: {invalid}')
|
|
100
102
|
prefix = to_camel_case(name.title())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
GraphQL output types and resolvers.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import collections
|
|
5
6
|
import functools
|
|
6
7
|
import inspect
|
|
@@ -67,7 +68,7 @@ class Column:
|
|
|
67
68
|
if cls is Column:
|
|
68
69
|
return lambda cls: cls.register(*scalars) or cls
|
|
69
70
|
# strawberry#1921: scalar python names are prepended to column name
|
|
70
|
-
generic = cls
|
|
71
|
+
generic = issubclass(cls, Generic)
|
|
71
72
|
for scalar in scalars:
|
|
72
73
|
cls.registry[scalar] = cls[scalar_map.get(scalar, scalar)] if generic else cls
|
|
73
74
|
|
|
@@ -174,8 +175,7 @@ class OrdinalColumn(NominalColumn[T]):
|
|
|
174
175
|
|
|
175
176
|
@Column.register(str)
|
|
176
177
|
@strawberry.type(name='ingColumn', description="column of strings")
|
|
177
|
-
class StringColumn(OrdinalColumn[T]):
|
|
178
|
-
...
|
|
178
|
+
class StringColumn(OrdinalColumn[T]): ...
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
@strawberry.type
|
|
@@ -6,6 +6,7 @@ Copy and customize as needed. Demonstrates:
|
|
|
6
6
|
* datasets, scanners, and tables
|
|
7
7
|
* filtering and projection
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import json
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
import pyarrow.dataset as ds
|
|
@@ -13,7 +14,7 @@ from starlette.config import Config
|
|
|
13
14
|
from graphique.inputs import Expression
|
|
14
15
|
from graphique import GraphQL
|
|
15
16
|
|
|
16
|
-
config = Config('.env')
|
|
17
|
+
config = Config('.env' if Path('.env').is_file() else None)
|
|
17
18
|
PARQUET_PATH = Path(config('PARQUET_PATH')).resolve()
|
|
18
19
|
FEDERATED = config('FEDERATED', default='')
|
|
19
20
|
DEBUG = config('DEBUG', cast=bool, default=False)
|
|
@@ -8,21 +8,24 @@ Second, the partitioned dataset is rewritten to merge parts. Often the built-in
|
|
|
8
8
|
sufficient once partitioned, but there is a `fragments` option to optimize for memory or show
|
|
9
9
|
progress on the second pass.
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
import argparse
|
|
12
13
|
import shutil
|
|
14
|
+
from collections.abc import Iterable
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
import pyarrow.dataset as ds
|
|
15
17
|
from tqdm import tqdm # type: ignore
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
def sort_key(name: str) -> tuple:
|
|
21
|
+
"""Parse sort order."""
|
|
22
|
+
return name.lstrip('-'), ('descending' if name.startswith('-') else 'ascending')
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
def write_batches(scanner: ds.Scanner, base_dir: str, *partitioning: str, **options):
|
|
19
26
|
"""Partition dataset by batches."""
|
|
20
|
-
options.update(
|
|
21
|
-
|
|
22
|
-
partitioning=partitioning,
|
|
23
|
-
partitioning_flavor=options.get('partitioning_flavor', 'hive'),
|
|
24
|
-
existing_data_behavior='overwrite_or_ignore',
|
|
25
|
-
)
|
|
27
|
+
options.update(format='parquet', partitioning=partitioning)
|
|
28
|
+
options.update(partitioning_flavor='hive', existing_data_behavior='overwrite_or_ignore')
|
|
26
29
|
with tqdm(total=scanner.count_rows(), desc="Batches") as pbar:
|
|
27
30
|
for index, batch in enumerate(scanner.to_batches()):
|
|
28
31
|
options['basename_template'] = f'part-{index}-{{i}}.parquet'
|
|
@@ -30,24 +33,31 @@ def write_batches(scanner: ds.Scanner, base_dir: str, *partitioning: str, **opti
|
|
|
30
33
|
pbar.update(len(batch))
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
def write_fragments(dataset: ds.Dataset, base_dir: str, **options):
|
|
36
|
+
def write_fragments(dataset: ds.Dataset, base_dir: str, sorting=(), **options):
|
|
34
37
|
"""Rewrite partition files by fragment to consolidate."""
|
|
38
|
+
options['format'] = 'parquet'
|
|
35
39
|
exprs = {Path(frag.path).parent: frag.partition_expression for frag in dataset.get_fragments()}
|
|
36
40
|
offset = len(dataset.partitioning.schema)
|
|
37
41
|
for path in tqdm(exprs, desc="Fragments"):
|
|
38
42
|
part_dir = Path(base_dir, *path.parts[-offset:])
|
|
39
|
-
|
|
43
|
+
part = dataset.filter(exprs[path])
|
|
44
|
+
ds.write_dataset(part.sort_by(sorting) if sorting else part, part_dir, **options)
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
def partition(
|
|
43
|
-
scanner: ds.Scanner,
|
|
48
|
+
scanner: ds.Scanner,
|
|
49
|
+
base_dir: str,
|
|
50
|
+
*partitioning: str,
|
|
51
|
+
fragments: bool = False,
|
|
52
|
+
sort: Iterable[str] = (),
|
|
53
|
+
**options,
|
|
44
54
|
):
|
|
45
55
|
"""Partition dataset by keys."""
|
|
46
56
|
temp = Path(base_dir) / 'temp'
|
|
47
57
|
write_batches(scanner, str(temp), *partitioning)
|
|
48
58
|
dataset = ds.dataset(temp, partitioning='hive')
|
|
49
|
-
if fragments:
|
|
50
|
-
write_fragments(dataset, base_dir, **options)
|
|
59
|
+
if fragments or sort:
|
|
60
|
+
write_fragments(dataset, base_dir, tuple(map(sort_key, sort)), **options)
|
|
51
61
|
else:
|
|
52
62
|
options.update(partitioning_flavor='hive', existing_data_behavior='overwrite_or_ignore')
|
|
53
63
|
with tqdm(desc="Partitions"):
|
|
@@ -60,8 +70,9 @@ parser.add_argument('src', help="source path")
|
|
|
60
70
|
parser.add_argument('dest', help="destination path")
|
|
61
71
|
parser.add_argument('partitioning', nargs='+', help="partition keys")
|
|
62
72
|
parser.add_argument('--fragments', action='store_true', help="iterate over fragments")
|
|
73
|
+
parser.add_argument('--sort', nargs='*', default=(), help="sort keys; will load fragments")
|
|
63
74
|
|
|
64
75
|
if __name__ == '__main__':
|
|
65
76
|
args = parser.parse_args()
|
|
66
77
|
dataset = ds.dataset(args.src, partitioning='hive')
|
|
67
|
-
partition(dataset, args.dest, *args.partitioning, fragments=args.fragments)
|
|
78
|
+
partition(dataset, args.dest, *args.partitioning, fragments=args.fragments, sort=args.sort)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: graphique
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5
|
|
4
4
|
Summary: GraphQL service for arrow tables and parquet data sets.
|
|
5
5
|
Author-email: Aric Coady <aric.coady@gmail.com>
|
|
6
6
|
License: Copyright 2022 Aric Coady
|
|
@@ -40,7 +40,7 @@ Classifier: Typing :: Typed
|
|
|
40
40
|
Requires-Python: >=3.9
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
License-File: LICENSE.txt
|
|
43
|
-
Requires-Dist: pyarrow>=
|
|
43
|
+
Requires-Dist: pyarrow>=15
|
|
44
44
|
Requires-Dist: strawberry-graphql[asgi,cli]>=0.198
|
|
45
45
|
Provides-Extra: server
|
|
46
46
|
Requires-Dist: uvicorn[standard]; extra == "server"
|
|
@@ -54,7 +54,6 @@ Requires-Dist: tqdm; extra == "cli"
|
|
|
54
54
|
[](https://github.com/coady/graphique/actions)
|
|
55
55
|
[](https://codecov.io/gh/coady/graphique/)
|
|
56
56
|
[](https://github.com/coady/graphique/security/code-scanning)
|
|
57
|
-
[](https://pypi.org/project/black/)
|
|
58
57
|
[](https://github.com/astral-sh/ruff)
|
|
59
58
|
[](https://mypy-lang.org/)
|
|
60
59
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphique"
|
|
7
|
-
dynamic = ["version"]
|
|
7
|
+
dynamic = ["version", "dependencies"]
|
|
8
8
|
description = "GraphQL service for arrow tables and parquet data sets."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -28,7 +28,6 @@ classifiers = [
|
|
|
28
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
29
|
"Typing :: Typed",
|
|
30
30
|
]
|
|
31
|
-
dependencies = ["pyarrow>=14", "strawberry-graphql[asgi,cli]>=0.198"]
|
|
32
31
|
|
|
33
32
|
[project.urls]
|
|
34
33
|
Homepage = "https://github.com/coady/graphique"
|
|
@@ -45,16 +44,18 @@ packages = ["graphique"]
|
|
|
45
44
|
|
|
46
45
|
[tool.setuptools.dynamic]
|
|
47
46
|
version = {attr = "graphique.__version__"}
|
|
47
|
+
dependencies = {file = "requirements.in"}
|
|
48
48
|
|
|
49
49
|
[tool.setuptools.package-data]
|
|
50
50
|
graphique = ["py.typed"]
|
|
51
51
|
|
|
52
|
-
[tool.
|
|
52
|
+
[tool.ruff]
|
|
53
53
|
line-length = 100
|
|
54
|
-
|
|
54
|
+
extend-include = ["*.ipynb"]
|
|
55
55
|
|
|
56
|
-
[tool.ruff]
|
|
57
|
-
|
|
56
|
+
[tool.ruff.format]
|
|
57
|
+
preview = true
|
|
58
|
+
quote-style = "preserve"
|
|
58
59
|
|
|
59
60
|
[[tool.mypy.overrides]]
|
|
60
61
|
module = ["pyarrow.*", "strawberry.*", "starlette.*"]
|
|
@@ -39,20 +39,14 @@ def test_search(dsclient):
|
|
|
39
39
|
assert data == {'filter': {'length': 41700}}
|
|
40
40
|
data = dsclient.execute('{ filter(zipcode: {}) { row { zipcode } } }')
|
|
41
41
|
assert data == {'filter': {'row': {'zipcode': 501}}}
|
|
42
|
-
data = dsclient.execute(
|
|
43
|
-
|
|
44
|
-
length } } }'''
|
|
45
|
-
)
|
|
42
|
+
data = dsclient.execute("""{ filter(zipcode: {gt: 90000}) { filter(state: {eq: "CA"}) {
|
|
43
|
+
length } } }""")
|
|
46
44
|
assert data == {'filter': {'filter': {'length': 2647}}}
|
|
47
|
-
data = dsclient.execute(
|
|
48
|
-
|
|
49
|
-
length row { zipcode } } } }'''
|
|
50
|
-
)
|
|
45
|
+
data = dsclient.execute("""{ filter(zipcode: {gt: 90000}) { filter(state: {eq: "CA"}) {
|
|
46
|
+
length row { zipcode } } } }""")
|
|
51
47
|
assert data == {'filter': {'filter': {'length': 2647, 'row': {'zipcode': 90001}}}}
|
|
52
|
-
data = dsclient.execute(
|
|
53
|
-
|
|
54
|
-
group(by: "county") { length } } } }'''
|
|
55
|
-
)
|
|
48
|
+
data = dsclient.execute("""{ filter(zipcode: {lt: 90000}) { filter(state: {eq: "CA"}) {
|
|
49
|
+
group(by: "county") { length } } } }""")
|
|
56
50
|
assert data == {'filter': {'filter': {'group': {'length': 0}}}}
|
|
57
51
|
|
|
58
52
|
|
|
@@ -75,13 +69,11 @@ def test_slice(dsclient):
|
|
|
75
69
|
|
|
76
70
|
def test_group(dsclient):
|
|
77
71
|
data = dsclient.execute(
|
|
78
|
-
|
|
72
|
+
"""{ group(by: ["state"], aggregate: {min: {name: "county"}}) { row { state county } } }"""
|
|
79
73
|
)
|
|
80
74
|
assert data == {'group': {'row': {'state': 'NY', 'county': 'Albany'}}}
|
|
81
|
-
data = dsclient.execute(
|
|
82
|
-
|
|
83
|
-
column(name: "c") { ... on LongColumn { values } } } } }'''
|
|
84
|
-
)
|
|
75
|
+
data = dsclient.execute("""{ group(by: ["state"], counts: "c") { slice(length: 1) {
|
|
76
|
+
column(name: "c") { ... on LongColumn { values } } } } }""")
|
|
85
77
|
assert data == {'group': {'slice': {'column': {'values': [2205]}}}}
|
|
86
78
|
data = dsclient.execute(
|
|
87
79
|
'{ group(by: ["state"], aggregate: {first: {name: "county"}}) { row { county } } }'
|
|
@@ -92,37 +84,31 @@ def test_group(dsclient):
|
|
|
92
84
|
)
|
|
93
85
|
assert data['group']['row']['county']
|
|
94
86
|
data = dsclient.execute(
|
|
95
|
-
|
|
96
|
-
column(name: "zipcode") { ... on FloatColumn { values } } } } }
|
|
97
|
-
)
|
|
87
|
+
"""{ group(by: ["state"], aggregate: {mean: {name: "zipcode"}}) { slice(length: 1) {
|
|
88
|
+
column(name: "zipcode") { ... on FloatColumn { values } } } } }""")
|
|
98
89
|
assert data == {'group': {'slice': {'column': {'values': [pytest.approx(12614.62721)]}}}}
|
|
99
90
|
data = dsclient.execute(
|
|
100
|
-
|
|
101
|
-
slice(length: 1) { column(name: "zipcode") { ... on FloatColumn { values } } } } } }
|
|
102
|
-
)
|
|
91
|
+
"""{ group(by: ["state"], aggregate: {list: {name: "zipcode"}}) { aggregate(mean: {name: "zipcode"}) {
|
|
92
|
+
slice(length: 1) { column(name: "zipcode") { ... on FloatColumn { values } } } } } }""")
|
|
103
93
|
assert data == {
|
|
104
94
|
'group': {'aggregate': {'slice': {'column': {'values': [pytest.approx(12614.62721)]}}}}
|
|
105
95
|
}
|
|
106
|
-
data = dsclient.execute(
|
|
107
|
-
|
|
108
|
-
column(name: "st") { ... on StringColumn { values } } } }'''
|
|
109
|
-
)
|
|
96
|
+
data = dsclient.execute("""{ group(aggregate: {min: {alias: "st", name: "state"}}) {
|
|
97
|
+
column(name: "st") { ... on StringColumn { values } } } }""")
|
|
110
98
|
assert data == {'group': {'column': {'values': ['AK']}}}
|
|
111
99
|
|
|
112
100
|
|
|
113
101
|
def test_list(partclient):
|
|
114
102
|
data = partclient.execute(
|
|
115
|
-
|
|
116
|
-
tables { row { state } column(name: "counties") { length } } } }
|
|
117
|
-
)
|
|
103
|
+
"""{ group(by: "state", aggregate: {distinct: {alias: "counties", name: "county"}}) {
|
|
104
|
+
tables { row { state } column(name: "counties") { length } } } } """)
|
|
118
105
|
(table,) = [table for table in data['group']['tables'] if table['row']['state'] == 'PR']
|
|
119
106
|
assert table == {'row': {'state': 'PR'}, 'column': {'length': 78}}
|
|
120
|
-
data = partclient.execute(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert table == {'row': {'north': 0}, 'columns': {'west': {'length': 2}}}
|
|
107
|
+
data = partclient.execute("""{ group(by: "north", aggregate: {distinct: {name: "west"}}) {
|
|
108
|
+
tables { row { north } columns { west { length } } } } }""")
|
|
109
|
+
tables = data['group']['tables']
|
|
110
|
+
assert {table['row']['north'] for table in tables} == {0, 1}
|
|
111
|
+
assert [table['columns'] for table in tables] == [{'west': {'length': 2}}] * 2
|
|
126
112
|
|
|
127
113
|
|
|
128
114
|
def test_fragments(partclient):
|
|
@@ -146,14 +132,11 @@ def test_fragments(partclient):
|
|
|
146
132
|
)
|
|
147
133
|
assert data == {'group': {'length': 1, 'row': {'state': 'AK'}}}
|
|
148
134
|
data = partclient.execute(
|
|
149
|
-
|
|
150
|
-
length column(name: "city") { type } } }
|
|
151
|
-
)
|
|
135
|
+
"""{ group(by: ["north", "west"], aggregate: {distinct: {name: "city"}, mean: {name: "zipcode"}}) {
|
|
136
|
+
length column(name: "city") { type } } }""")
|
|
152
137
|
assert data == {'group': {'length': 4, 'column': {'type': 'large_list<item: string>'}}}
|
|
153
|
-
data = partclient.execute(
|
|
154
|
-
|
|
155
|
-
column(name: "west") { ... on LongColumn { values } } } }'''
|
|
156
|
-
)
|
|
138
|
+
data = partclient.execute("""{ group(by: "north", aggregate: {countDistinct: {name: "west"}}) {
|
|
139
|
+
column(name: "west") { ... on LongColumn { values } } } }""")
|
|
157
140
|
assert data == {'group': {'column': {'values': [2, 2]}}}
|
|
158
141
|
data = partclient.execute(
|
|
159
142
|
'{ group(by: "north", counts: "c") { column(name: "c") { ... on LongColumn { values } } } }'
|
|
@@ -202,16 +185,12 @@ def test_scan(dsclient):
|
|
|
202
185
|
dsclient.execute('{ scan(filter: {name: "state", value: "CA"}) { length } }')
|
|
203
186
|
with pytest.raises(ValueError, match="name or alias"):
|
|
204
187
|
dsclient.execute('{ scan(columns: {}) { length } }')
|
|
205
|
-
data = dsclient.execute(
|
|
206
|
-
'''{ scan(filter: {eq: [{name: "state"}, {value: "CA"}]})
|
|
188
|
+
data = dsclient.execute("""{ scan(filter: {eq: [{name: "state"}, {value: "CA"}]})
|
|
207
189
|
{ scan(filter: {eq: [{name: "county"}, {value: "Santa Clara"}]})
|
|
208
|
-
{ length row { county } } } }
|
|
209
|
-
)
|
|
190
|
+
{ length row { county } } } }""")
|
|
210
191
|
assert data == {'scan': {'scan': {'length': 108, 'row': {'county': 'Santa Clara'}}}}
|
|
211
|
-
data = dsclient.execute(
|
|
212
|
-
|
|
213
|
-
{eq: [{name: "county"}, {value: "Santa Clara"}]}]}) { length } }'''
|
|
214
|
-
)
|
|
192
|
+
data = dsclient.execute("""{ scan(filter: {or: [{eq: [{name: "state"}, {value: "CA"}]},
|
|
193
|
+
{eq: [{name: "county"}, {value: "Santa Clara"}]}]}) { length } }""")
|
|
215
194
|
assert data == {'scan': {'length': 2647}}
|
|
216
195
|
|
|
217
196
|
|
|
@@ -254,32 +233,26 @@ def test_federation(fedclient):
|
|
|
254
233
|
assert data['zipcodes'] == {'__typename': 'ZipcodesTable', 'length': 41700}
|
|
255
234
|
assert data['zipDb'] == {'__typename': 'ZipDbTable', 'length': 42724}
|
|
256
235
|
|
|
257
|
-
data = fedclient.execute(
|
|
258
|
-
|
|
259
|
-
join(right: "zip_db", keys: "zipcode", rightKeys: "zip") { length schema { names } } } } }'''
|
|
260
|
-
)
|
|
236
|
+
data = fedclient.execute("""{ zipcodes { scan(columns: {name: "zipcode", cast: "int64"}) {
|
|
237
|
+
join(right: "zip_db", keys: "zipcode", rightKeys: "zip") { length schema { names } } } } }""")
|
|
261
238
|
table = data['zipcodes']['scan']['join']
|
|
262
239
|
assert table['length'] == 41700
|
|
263
240
|
assert set(table['schema']['names']) > {'zipcode', 'timezone', 'latitude'}
|
|
264
241
|
data = fedclient.execute(
|
|
265
|
-
|
|
266
|
-
join(right: "zip_db", keys: "zip", joinType: "right outer") { length schema { names } } } } }
|
|
267
|
-
)
|
|
242
|
+
"""{ zipcodes { scan(columns: {alias: "zip", name: "zipcode", cast: "int64"}) {
|
|
243
|
+
join(right: "zip_db", keys: "zip", joinType: "right outer") { length schema { names } } } } }""")
|
|
268
244
|
table = data['zipcodes']['scan']['join']
|
|
269
245
|
assert table['length'] == 42724
|
|
270
246
|
assert set(table['schema']['names']) > {'zip', 'timezone', 'latitude'}
|
|
271
247
|
|
|
272
248
|
data = fedclient.execute(
|
|
273
|
-
|
|
274
|
-
... on ZipcodesTable { length row { state } schema { names } } } }
|
|
275
|
-
)
|
|
249
|
+
"""{ _entities(representations: {__typename: "ZipcodesTable", zipcode: 90001}) {
|
|
250
|
+
... on ZipcodesTable { length row { state } schema { names } } } }""")
|
|
276
251
|
assert data == {
|
|
277
252
|
'_entities': [{'length': 1, 'row': {'state': 'CA'}, 'schema': {'names': ['state']}}]
|
|
278
253
|
}
|
|
279
|
-
data = fedclient.execute(
|
|
280
|
-
|
|
281
|
-
takeFrom(field: "zipcodes") { __typename column(name: "state") { length } } } } } } }'''
|
|
282
|
-
)
|
|
254
|
+
data = fedclient.execute("""{ states { filter(state: {eq: "CA"}) { columns { indices {
|
|
255
|
+
takeFrom(field: "zipcodes") { __typename column(name: "state") { length } } } } } } }""")
|
|
283
256
|
table = data['states']['filter']['columns']['indices']['takeFrom']
|
|
284
257
|
assert table == {'__typename': 'ZipcodesTable', 'column': {'length': 2647}}
|
|
285
258
|
|