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.
Files changed (25) hide show
  1. {graphique-1.4 → graphique-1.5}/PKG-INFO +2 -3
  2. {graphique-1.4 → graphique-1.5}/README.md +0 -1
  3. {graphique-1.4 → graphique-1.5}/graphique/__init__.py +1 -1
  4. {graphique-1.4 → graphique-1.5}/graphique/core.py +9 -5
  5. {graphique-1.4 → graphique-1.5}/graphique/inputs.py +1 -0
  6. {graphique-1.4 → graphique-1.5}/graphique/interface.py +11 -8
  7. {graphique-1.4 → graphique-1.5}/graphique/middleware.py +3 -1
  8. {graphique-1.4 → graphique-1.5}/graphique/models.py +3 -3
  9. {graphique-1.4 → graphique-1.5}/graphique/scalars.py +1 -0
  10. {graphique-1.4 → graphique-1.5}/graphique/service.py +2 -1
  11. {graphique-1.4 → graphique-1.5}/graphique/shell.py +23 -12
  12. {graphique-1.4 → graphique-1.5}/graphique.egg-info/PKG-INFO +2 -3
  13. {graphique-1.4 → graphique-1.5}/graphique.egg-info/SOURCES.txt +1 -0
  14. {graphique-1.4 → graphique-1.5}/graphique.egg-info/requires.txt +1 -1
  15. {graphique-1.4 → graphique-1.5}/pyproject.toml +7 -6
  16. graphique-1.5/requirements.in +2 -0
  17. {graphique-1.4 → graphique-1.5}/tests/test_dataset.py +38 -65
  18. {graphique-1.4 → graphique-1.5}/tests/test_models.py +76 -134
  19. {graphique-1.4 → graphique-1.5}/tests/test_service.py +91 -152
  20. {graphique-1.4 → graphique-1.5}/LICENSE.txt +0 -0
  21. {graphique-1.4 → graphique-1.5}/graphique/py.typed +0 -0
  22. {graphique-1.4 → graphique-1.5}/graphique.egg-info/dependency_links.txt +0 -0
  23. {graphique-1.4 → graphique-1.5}/graphique.egg-info/top_level.txt +0 -0
  24. {graphique-1.4 → graphique-1.5}/setup.cfg +0 -0
  25. {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.4
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>=14
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
  [![image](https://github.com/coady/graphique/workflows/build/badge.svg)](https://github.com/coady/graphique/actions)
55
55
  [![image](https://codecov.io/gh/coady/graphique/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/graphique/)
56
56
  [![image](https://github.com/coady/graphique/workflows/codeql/badge.svg)](https://github.com/coady/graphique/security/code-scanning)
57
- [![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://pypi.org/project/black/)
58
57
  [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
59
58
  [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
60
59
 
@@ -5,7 +5,6 @@
5
5
  [![image](https://github.com/coady/graphique/workflows/build/badge.svg)](https://github.com/coady/graphique/actions)
6
6
  [![image](https://codecov.io/gh/coady/graphique/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/graphique/)
7
7
  [![image](https://github.com/coady/graphique/workflows/codeql/badge.svg)](https://github.com/coady/graphique/security/code-scanning)
8
- [![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://pypi.org/project/black/)
9
8
  [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
10
9
  [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
11
10
 
@@ -1,3 +1,3 @@
1
1
  from .middleware import GraphQL # noqa: F401
2
2
 
3
- __version__ = '1.4'
3
+ __version__ = '1.5'
@@ -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, array: pa.float64(), bins: pa.list_(pa.float64()), right: pa.bool_() # type: ignore
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.update(Table.columns(table))
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.update({agg.alias: func(self[agg.name], **agg.options) for agg in aggs})
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.update({name: table[name][0] for name in table.schema.names})
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.update({name: self[name][index].values for name in lists})
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:
@@ -1,6 +1,7 @@
1
1
  """
2
2
  GraphQL input types.
3
3
  """
4
+
4
5
  import functools
5
6
  import inspect
6
7
  import operator
@@ -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, info: Info, by: list[str] = [], counts: str = '', aggregate: HashAggregates = {} # type: ignore
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.update(T.aggregate(table, counts=counts, **aggs))
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.update(T.columns(pa.RecordBatch.from_pydict(arrays)))
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.update({field.name: pa.array(columns[field.name], field.type) for field in schema})
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.update({col.alias or '.'.join(col.name): col.to_arrow() for col in columns})
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.__strawberry_definition__.is_generic
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
@@ -1,6 +1,7 @@
1
1
  """
2
2
  GraphQL scalars.
3
3
  """
4
+
4
5
  from datetime import date, datetime, time, timedelta
5
6
  from decimal import Decimal
6
7
  import pyarrow as pa
@@ -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
- format=options.get('format', 'parquet'),
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
- ds.write_dataset(dataset.filter(exprs[path]), part_dir, **options)
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, base_dir: str, *partitioning: str, fragments: bool = False, **options
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.4
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>=14
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
  [![image](https://github.com/coady/graphique/workflows/build/badge.svg)](https://github.com/coady/graphique/actions)
55
55
  [![image](https://codecov.io/gh/coady/graphique/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/graphique/)
56
56
  [![image](https://github.com/coady/graphique/workflows/codeql/badge.svg)](https://github.com/coady/graphique/security/code-scanning)
57
- [![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://pypi.org/project/black/)
58
57
  [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
59
58
  [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
60
59
 
@@ -1,6 +1,7 @@
1
1
  LICENSE.txt
2
2
  README.md
3
3
  pyproject.toml
4
+ requirements.in
4
5
  graphique/__init__.py
5
6
  graphique/core.py
6
7
  graphique/inputs.py
@@ -1,4 +1,4 @@
1
- pyarrow>=14
1
+ pyarrow>=15
2
2
  strawberry-graphql[asgi,cli]>=0.198
3
3
 
4
4
  [cli]
@@ -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.black]
52
+ [tool.ruff]
53
53
  line-length = 100
54
- skip-string-normalization = true
54
+ extend-include = ["*.ipynb"]
55
55
 
56
- [tool.ruff]
57
- ignore = ["E501"]
56
+ [tool.ruff.format]
57
+ preview = true
58
+ quote-style = "preserve"
58
59
 
59
60
  [[tool.mypy.overrides]]
60
61
  module = ["pyarrow.*", "strawberry.*", "starlette.*"]
@@ -0,0 +1,2 @@
1
+ pyarrow>=15
2
+ strawberry-graphql[asgi,cli]>=0.198
@@ -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
- '''{ filter(zipcode: {gt: 90000}) { filter(state: {eq: "CA"}) {
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
- '''{ filter(zipcode: {gt: 90000}) { filter(state: {eq: "CA"}) {
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
- '''{ filter(zipcode: {lt: 90000}) { filter(state: {eq: "CA"}) {
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
- '''{ group(by: ["state"], aggregate: {min: {name: "county"}}) { row { state county } } }'''
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
- '''{ group(by: ["state"], counts: "c") { slice(length: 1) {
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
- '''{ group(by: ["state"], aggregate: {mean: {name: "zipcode"}}) { slice(length: 1) {
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
- '''{ group(by: ["state"], aggregate: {list: {name: "zipcode"}}) { aggregate(mean: {name: "zipcode"}) {
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
- '''{ group(aggregate: {min: {alias: "st", name: "state"}}) {
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
- '''{ group(by: "state", aggregate: {distinct: {alias: "counties", name: "county"}}) {
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
- '''{ group(by: "north", aggregate: {distinct: {name: "west"}}) {
122
- tables { row { north } columns { west { length } } } } }'''
123
- )
124
- table = data['group']['tables'][0]
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
- '''{ group(by: ["north", "west"], aggregate: {distinct: {name: "city"}, mean: {name: "zipcode"}}) {
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
- '''{ group(by: "north", aggregate: {countDistinct: {name: "west"}}) {
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
- '''{ scan(filter: {or: [{eq: [{name: "state"}, {value: "CA"}]},
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
- '''{ zipcodes { scan(columns: {name: "zipcode", cast: "int64"}) {
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
- '''{ zipcodes { scan(columns: {alias: "zip", name: "zipcode", cast: "int64"}) {
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
- '''{ _entities(representations: {__typename: "ZipcodesTable", zipcode: 90001}) {
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
- '''{ states { filter(state: {eq: "CA"}) { columns { indices {
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