graphique 1.8__tar.gz → 1.9__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 (26) hide show
  1. {graphique-1.8 → graphique-1.9}/PKG-INFO +6 -20
  2. {graphique-1.8 → graphique-1.9}/graphique/core.py +9 -10
  3. {graphique-1.8 → graphique-1.9}/graphique/inputs.py +166 -165
  4. {graphique-1.8 → graphique-1.9}/graphique/interface.py +16 -14
  5. {graphique-1.8 → graphique-1.9}/graphique/middleware.py +4 -5
  6. {graphique-1.8 → graphique-1.9}/graphique/models.py +26 -27
  7. {graphique-1.8 → graphique-1.9}/graphique/scalars.py +3 -2
  8. {graphique-1.8 → graphique-1.9}/graphique/shell.py +2 -2
  9. {graphique-1.8 → graphique-1.9}/graphique.egg-info/PKG-INFO +6 -20
  10. {graphique-1.8 → graphique-1.9}/graphique.egg-info/requires.txt +1 -1
  11. {graphique-1.8 → graphique-1.9}/pyproject.toml +3 -5
  12. {graphique-1.8 → graphique-1.9}/requirements.in +1 -1
  13. {graphique-1.8 → graphique-1.9}/tests/test_core.py +2 -0
  14. {graphique-1.8 → graphique-1.9}/LICENSE.txt +0 -0
  15. {graphique-1.8 → graphique-1.9}/README.md +0 -0
  16. {graphique-1.8 → graphique-1.9}/graphique/__init__.py +0 -0
  17. {graphique-1.8 → graphique-1.9}/graphique/py.typed +0 -0
  18. {graphique-1.8 → graphique-1.9}/graphique/service.py +0 -0
  19. {graphique-1.8 → graphique-1.9}/graphique.egg-info/SOURCES.txt +0 -0
  20. {graphique-1.8 → graphique-1.9}/graphique.egg-info/dependency_links.txt +0 -0
  21. {graphique-1.8 → graphique-1.9}/graphique.egg-info/top_level.txt +0 -0
  22. {graphique-1.8 → graphique-1.9}/setup.cfg +0 -0
  23. {graphique-1.8 → graphique-1.9}/tests/test_bench.py +0 -0
  24. {graphique-1.8 → graphique-1.9}/tests/test_dataset.py +0 -0
  25. {graphique-1.8 → graphique-1.9}/tests/test_models.py +0 -0
  26. {graphique-1.8 → graphique-1.9}/tests/test_service.py +0 -0
@@ -1,22 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: graphique
3
- Version: 1.8
3
+ Version: 1.9
4
4
  Summary: GraphQL service for arrow tables and parquet data sets.
5
5
  Author-email: Aric Coady <aric.coady@gmail.com>
6
- License: Copyright 2022 Aric Coady
7
-
8
- Licensed under the Apache License, Version 2.0 (the "License");
9
- you may not use this file except in compliance with the License.
10
- You may obtain a copy of the License at
11
-
12
- http://www.apache.org/licenses/LICENSE-2.0
13
-
14
- Unless required by applicable law or agreed to in writing, software
15
- distributed under the License is distributed on an "AS IS" BASIS,
16
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- See the License for the specific language governing permissions and
18
- limitations under the License.
19
-
6
+ License: Apache-2.0
20
7
  Project-URL: Homepage, https://github.com/coady/graphique
21
8
  Project-URL: Documentation, https://coady.github.io/graphique
22
9
  Project-URL: Changelog, https://github.com/coady/graphique/blob/main/CHANGELOG.md
@@ -25,10 +12,8 @@ Keywords: graphql,arrow,parquet
25
12
  Classifier: Development Status :: 5 - Production/Stable
26
13
  Classifier: Intended Audience :: Developers
27
14
  Classifier: Intended Audience :: Science/Research
28
- Classifier: License :: OSI Approved :: Apache Software License
29
15
  Classifier: Operating System :: OS Independent
30
16
  Classifier: Programming Language :: Python :: 3
31
- Classifier: Programming Language :: Python :: 3.9
32
17
  Classifier: Programming Language :: Python :: 3.10
33
18
  Classifier: Programming Language :: Python :: 3.11
34
19
  Classifier: Programming Language :: Python :: 3.12
@@ -38,10 +23,10 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
38
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
39
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
25
  Classifier: Typing :: Typed
41
- Requires-Python: >=3.9
26
+ Requires-Python: >=3.10
42
27
  Description-Content-Type: text/markdown
43
28
  License-File: LICENSE.txt
44
- Requires-Dist: pyarrow>=18
29
+ Requires-Dist: pyarrow>=20
45
30
  Requires-Dist: strawberry-graphql[asgi,cli]>=0.236
46
31
  Requires-Dist: numpy
47
32
  Requires-Dist: isodate>=0.7
@@ -49,6 +34,7 @@ Provides-Extra: server
49
34
  Requires-Dist: uvicorn[standard]; extra == "server"
50
35
  Provides-Extra: cli
51
36
  Requires-Dist: tqdm; extra == "cli"
37
+ Dynamic: license-file
52
38
 
53
39
  [![image](https://img.shields.io/pypi/v/graphique.svg)](https://pypi.org/project/graphique/)
54
40
  ![image](https://img.shields.io/pypi/pyversions/graphique.svg)
@@ -14,7 +14,7 @@ import operator
14
14
  import json
15
15
  from collections.abc import Callable, Iterable, Iterator, Mapping
16
16
  from dataclasses import dataclass
17
- from typing import Optional, Union, get_type_hints
17
+ from typing import TypeAlias, get_type_hints
18
18
  import numpy as np
19
19
  import pyarrow as pa
20
20
  import pyarrow.acero as ac
@@ -22,8 +22,8 @@ import pyarrow.compute as pc
22
22
  import pyarrow.dataset as ds
23
23
  from typing_extensions import Self
24
24
 
25
- Array = Union[pa.Array, pa.ChunkedArray]
26
- Batch = Union[pa.RecordBatch, pa.Table]
25
+ Array: TypeAlias = pa.Array | pa.ChunkedArray
26
+ Batch: TypeAlias = pa.RecordBatch | pa.Table
27
27
  bit_any = functools.partial(functools.reduce, operator.or_)
28
28
  bit_all = functools.partial(functools.reduce, operator.and_)
29
29
 
@@ -64,12 +64,11 @@ class Agg:
64
64
  return self.option_map[func.removeprefix('hash_')](**self.options)
65
65
 
66
66
 
67
- @dataclass(frozen=True)
67
+ @dataclass(frozen=True, slots=True)
68
68
  class Compare:
69
69
  """Comparable wrapper for bisection search."""
70
70
 
71
71
  value: object
72
- __slots__ = ('value',) # slots keyword in Python >=3.10
73
72
 
74
73
  def __lt__(self, other):
75
74
  return self.value < other.as_py()
@@ -152,7 +151,7 @@ class ListChunk(pa.lib.BaseListArray):
152
151
  offsets = self.values.offsets.take(self.offsets)
153
152
  return type(self).from_arrays(offsets, self.values.values)
154
153
 
155
- def aggregate(self, **funcs: Optional[pc.FunctionOptions]) -> pa.RecordBatch:
154
+ def aggregate(self, **funcs: pc.FunctionOptions | None) -> pa.RecordBatch:
156
155
  """Return aggregated scalars by grouping each hash function on the parent indices.
157
156
 
158
157
  If there are empty or null scalars, then the result must be padded with null defaults and
@@ -241,7 +240,7 @@ class Column(pa.ChunkedArray):
241
240
  if not pa.types.is_dictionary(self.type):
242
241
  return self
243
242
  array = self if isinstance(self, pa.Array) else self.combine_chunks()
244
- return pc.rank(array.dictionary, 'ascending').take(array.indices)
243
+ return pc.rank(array.dictionary).take(array.indices)
245
244
 
246
245
  def pairwise_diff(self, period: int = 1) -> Array:
247
246
  """`pairwise_diff` with chunked array support."""
@@ -391,7 +390,7 @@ class Table(pa.Table):
391
390
  return Table.union(self, Table.from_counts(table, counts))
392
391
 
393
392
  def sort_indices(
394
- self, *names: str, length: Optional[int] = None, null_placement: str = 'at_end'
393
+ self, *names: str, length: int | None = None, null_placement: str = 'at_end'
395
394
  ) -> pa.Array:
396
395
  """Return indices which would sort the table by columns, optimized for fixed length."""
397
396
  func = functools.partial(pc.sort_indices, null_placement=null_placement)
@@ -404,7 +403,7 @@ class Table(pa.Table):
404
403
  def sort(
405
404
  self,
406
405
  *names: str,
407
- length: Optional[int] = None,
406
+ length: int | None = None,
408
407
  indices: str = '',
409
408
  null_placement: str = 'at_end',
410
409
  ) -> Batch:
@@ -521,7 +520,7 @@ class Table(pa.Table):
521
520
  offset += len(batch)
522
521
  yield pa.RecordBatch.from_pydict(columns)
523
522
 
524
- def split(self) -> Iterator[Optional[pa.RecordBatch]]:
523
+ def split(self) -> Iterator[pa.RecordBatch | None]:
525
524
  """Generate tables from splitting list scalars."""
526
525
  lists = Table.list_fields(self)
527
526
  scalars = set(self.schema.names) - lists
@@ -2,13 +2,14 @@
2
2
  GraphQL input types.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  import functools
6
7
  import inspect
7
8
  import operator
8
9
  from collections.abc import Callable, Iterable
9
10
  from datetime import date, datetime, time, timedelta
10
11
  from decimal import Decimal
11
- from typing import Generic, Optional, TypeVar, no_type_check
12
+ from typing import Generic, TypeVar, no_type_check
12
13
  import pyarrow as pa
13
14
  import pyarrow.compute as pc
14
15
  import pyarrow.dataset as ds
@@ -71,7 +72,7 @@ class provisional:
71
72
 
72
73
 
73
74
  def default_field(
74
- default=UNSET, func: Optional[Callable] = None, nullable: bool = False, **kwargs
75
+ default=UNSET, func: Callable | None = None, nullable: bool = False, **kwargs
75
76
  ) -> StrawberryField:
76
77
  """Use dataclass `default_factory` for `UNSET` or mutables."""
77
78
  if func is not None:
@@ -83,16 +84,16 @@ def default_field(
83
84
 
84
85
  @strawberry.input(description="predicates for scalars")
85
86
  class Filter(Generic[T], Input):
86
- eq: Optional[list[Optional[T]]] = default_field(
87
+ eq: list[T | None] | None = default_field(
87
88
  description="== or `isin`; `null` is equivalent to arrow `is_null`.", nullable=True
88
89
  )
89
- ne: Optional[T] = default_field(
90
+ ne: T | None = default_field(
90
91
  description="!=; `null` is equivalent to arrow `is_valid`.", nullable=True
91
92
  )
92
- lt: Optional[T] = default_field(description="<")
93
- le: Optional[T] = default_field(description="<=")
94
- gt: Optional[T] = default_field(description=r"\>")
95
- ge: Optional[T] = default_field(description=r"\>=")
93
+ lt: T | None = default_field(description="<")
94
+ le: T | None = default_field(description="<=")
95
+ gt: T | None = default_field(description=r"\>")
96
+ ge: T | None = default_field(description=r"\>=")
96
97
 
97
98
  nullables = {'eq', 'ne'}
98
99
 
@@ -133,7 +134,7 @@ class Pairwise(Field):
133
134
  class Index(Field):
134
135
  value: JSON
135
136
  start: Long = 0
136
- end: Optional[Long] = None
137
+ end: Long | None = None
137
138
 
138
139
 
139
140
  @strawberry.input
@@ -152,16 +153,20 @@ class Quantile(Field):
152
153
 
153
154
 
154
155
  @strawberry.input
155
- class Rank(Field):
156
+ class RankQuantile(Field):
156
157
  sort_keys: str = 'ascending'
157
158
  null_placement: str = 'at_end'
159
+
160
+
161
+ @strawberry.input
162
+ class Rank(RankQuantile):
158
163
  tiebreaker: str = 'first'
159
164
 
160
165
 
161
166
  @strawberry.input
162
167
  class Sort:
163
168
  by: list[str]
164
- length: Optional[Long] = None
169
+ length: Long | None = None
165
170
 
166
171
 
167
172
  @strawberry.input
@@ -174,12 +179,12 @@ class Ranked:
174
179
  class ListFunction(Input):
175
180
  deprecation = "List scalar functions will be moved to `scan(...: {list: ...})`"
176
181
 
177
- filter: 'Expression' = default_field({}, description="filter within list scalars")
178
- sort: Optional[Sort] = default_field(description="sort within list scalars")
179
- rank: Optional[Ranked] = default_field(description="select by dense rank within list scalars")
180
- index: Optional[Index] = default_field(func=pc.index, deprecation_reason=deprecation)
181
- mode: Optional[Mode] = default_field(func=pc.mode, deprecation_reason=deprecation)
182
- quantile: Optional[Quantile] = default_field(func=pc.quantile, deprecation_reason=deprecation)
182
+ filter: Expression = default_field({}, description="filter within list scalars")
183
+ sort: Sort | None = default_field(description="sort within list scalars")
184
+ rank: Ranked | None = default_field(description="select by dense rank within list scalars")
185
+ index: Index | None = default_field(func=pc.index, deprecation_reason=deprecation)
186
+ mode: Mode | None = default_field(func=pc.mode, deprecation_reason=deprecation)
187
+ quantile: Quantile | None = default_field(func=pc.quantile, deprecation_reason=deprecation)
183
188
 
184
189
  def keys(self):
185
190
  return set(super().keys()) - {'filter', 'sort', 'rank'}
@@ -252,10 +257,10 @@ class Diff(Input):
252
257
  """
253
258
 
254
259
  name: str
255
- less: Optional[float] = default_field(name='lt', description="<", nullable=True)
256
- less_equal: Optional[float] = default_field(name='le', description="<=", nullable=True)
257
- greater: Optional[float] = default_field(name='gt', description=r"\>", nullable=True)
258
- greater_equal: Optional[float] = default_field(name='ge', description=r"\>=", nullable=True)
260
+ less: float | None = default_field(name='lt', description="<", nullable=True)
261
+ less_equal: float | None = default_field(name='le', description="<=", nullable=True)
262
+ greater: float | None = default_field(name='gt', description=r"\>", nullable=True)
263
+ greater_equal: float | None = default_field(name='ge', description=r"\>=", nullable=True)
259
264
 
260
265
  nullables = {'less', 'less_equal', 'greater', 'greater_equal'}
261
266
 
@@ -275,7 +280,7 @@ class Expression:
275
280
  name: list[str] = default_field([], description="field name(s)")
276
281
  cast: str = strawberry.field(default='', description=f"cast as {links.type}")
277
282
  safe: bool = strawberry.field(default=True, description="check for conversion errors on cast")
278
- value: Optional[JSON] = default_field(
283
+ value: JSON | None = default_field(
279
284
  description="JSON scalar; also see typed scalars", nullable=True
280
285
  )
281
286
  kleene: bool = strawberry.field(default=False, description="use kleene logic for booleans")
@@ -288,58 +293,56 @@ class Expression:
288
293
  duration: list[timedelta] = default_field([])
289
294
  time_: list[time] = default_field([], name='time')
290
295
 
291
- eq: list['Expression'] = default_field([], description="==")
292
- ne: list['Expression'] = default_field([], description="!=")
293
- lt: list['Expression'] = default_field([], description="<")
294
- le: list['Expression'] = default_field([], description="<=")
295
- gt: list['Expression'] = default_field([], description=r"\>")
296
- ge: list['Expression'] = default_field([], description=r"\>=")
297
- inv: Optional['Expression'] = default_field(description="~")
298
-
299
- abs: Optional['Expression'] = default_field(func=pc.abs)
300
- add: list['Expression'] = default_field([], func=pc.add)
301
- divide: list['Expression'] = default_field([], func=pc.divide)
302
- multiply: list['Expression'] = default_field([], func=pc.multiply)
303
- negate: Optional['Expression'] = default_field(func=pc.negate)
304
- power: list['Expression'] = default_field([], func=pc.power)
305
- sign: Optional['Expression'] = default_field(func=pc.sign)
306
- subtract: list['Expression'] = default_field([], func=pc.subtract)
307
-
308
- bit_wise: Optional['BitWise'] = default_field(description="bit-wise functions")
309
- rounding: Optional['Rounding'] = default_field(description="rounding functions")
310
- log: Optional['Log'] = default_field(description="logarithmic functions")
311
- trig: Optional['Trig'] = default_field(description="trigonometry functions")
312
- element_wise: Optional['ElementWise'] = default_field(
313
- description="element-wise aggregate functions"
314
- )
315
-
316
- and_: list['Expression'] = default_field([], name='and', description="&")
317
- and_not: list['Expression'] = default_field([], func=pc.and_not)
318
- or_: list['Expression'] = default_field([], name='or', description="|")
319
- xor: list['Expression'] = default_field([], func=pc.xor)
320
-
321
- utf8: Optional['Utf8'] = default_field(description="utf8 string functions")
322
- string_is_ascii: Optional['Expression'] = default_field(func=pc.string_is_ascii)
323
- substring: Optional['MatchSubstring'] = default_field(description="match substring functions")
324
-
325
- binary: Optional['Binary'] = default_field(description="binary functions")
326
- set_lookup: Optional['SetLookup'] = default_field(description="set lookup functions")
327
-
328
- is_finite: Optional['Expression'] = default_field(func=pc.is_finite)
329
- is_inf: Optional['Expression'] = default_field(func=pc.is_inf)
330
- is_nan: Optional['Expression'] = default_field(func=pc.is_nan)
331
- true_unless_null: Optional['Expression'] = default_field(func=pc.true_unless_null)
332
-
333
- case_when: list['Expression'] = default_field([], func=pc.case_when)
334
- choose: list['Expression'] = default_field([], func=pc.choose)
335
- coalesce: list['Expression'] = default_field([], func=pc.coalesce)
336
- if_else: list['Expression'] = default_field([], func=pc.if_else)
337
-
338
- temporal: Optional['Temporal'] = default_field(description="temporal functions")
339
-
340
- replace_with_mask: list['Expression'] = default_field([], func=pc.replace_with_mask)
341
-
342
- list: Optional['Lists'] = default_field(description="list array functions")
296
+ eq: list[Expression] = default_field([], description="==")
297
+ ne: list[Expression] = default_field([], description="!=")
298
+ lt: list[Expression] = default_field([], description="<")
299
+ le: list[Expression] = default_field([], description="<=")
300
+ gt: list[Expression] = default_field([], description=r"\>")
301
+ ge: list[Expression] = default_field([], description=r"\>=")
302
+ inv: Expression | None = default_field(description="~")
303
+
304
+ abs: Expression | None = default_field(func=pc.abs)
305
+ add: list[Expression] = default_field([], func=pc.add)
306
+ divide: list[Expression] = default_field([], func=pc.divide)
307
+ multiply: list[Expression] = default_field([], func=pc.multiply)
308
+ negate: Expression | None = default_field(func=pc.negate)
309
+ power: list[Expression] = default_field([], func=pc.power)
310
+ sign: Expression | None = default_field(func=pc.sign)
311
+ subtract: list[Expression] = default_field([], func=pc.subtract)
312
+
313
+ bit_wise: BitWise | None = default_field(description="bit-wise functions")
314
+ rounding: Rounding | None = default_field(description="rounding functions")
315
+ log: Log | None = default_field(description="logarithmic functions")
316
+ trig: Trig | None = default_field(description="trigonometry functions")
317
+ element_wise: ElementWise | None = default_field(description="element-wise aggregate functions")
318
+
319
+ and_: list[Expression] = default_field([], name='and', description="&")
320
+ and_not: list[Expression] = default_field([], func=pc.and_not)
321
+ or_: list[Expression] = default_field([], name='or', description="|")
322
+ xor: list[Expression] = default_field([], func=pc.xor)
323
+
324
+ utf8: Utf8 | None = default_field(description="utf8 string functions")
325
+ string_is_ascii: Expression | None = default_field(func=pc.string_is_ascii)
326
+ substring: MatchSubstring | None = default_field(description="match substring functions")
327
+
328
+ binary: Binary | None = default_field(description="binary functions")
329
+ set_lookup: SetLookup | None = default_field(description="set lookup functions")
330
+
331
+ is_finite: Expression | None = default_field(func=pc.is_finite)
332
+ is_inf: Expression | None = default_field(func=pc.is_inf)
333
+ is_nan: Expression | None = default_field(func=pc.is_nan)
334
+ true_unless_null: Expression | None = default_field(func=pc.true_unless_null)
335
+
336
+ case_when: list[Expression] = default_field([], func=pc.case_when)
337
+ choose: list[Expression] = default_field([], func=pc.choose)
338
+ coalesce: list[Expression] = default_field([], func=pc.coalesce)
339
+ if_else: list[Expression] = default_field([], func=pc.if_else)
340
+
341
+ temporal: Temporal | None = default_field(description="temporal functions")
342
+
343
+ replace_with_mask: list[Expression] = default_field([], func=pc.replace_with_mask)
344
+
345
+ list: Lists | None = default_field(description="list array functions")
343
346
 
344
347
  unaries = ('inv', 'abs', 'negate', 'sign', 'string_is_ascii', 'is_finite', 'is_inf', 'is_nan')
345
348
  associatives = ('add', 'multiply', 'and_', 'or_', 'xor')
@@ -349,7 +352,7 @@ class Expression:
349
352
  groups = ('bit_wise', 'rounding', 'log', 'trig', 'element_wise', 'utf8', 'substring', 'binary')
350
353
  groups += ('set_lookup', 'temporal', 'list') # type: ignore
351
354
 
352
- def to_arrow(self) -> Optional[ds.Expression]:
355
+ def to_arrow(self) -> ds.Expression | None:
353
356
  """Transform GraphQL expression into a dataset expression."""
354
357
  fields = []
355
358
  if self.name:
@@ -457,11 +460,11 @@ class BitWise(Fields):
457
460
 
458
461
  @strawberry.input(description="Rounding functions.")
459
462
  class Rounding(Fields):
460
- ceil: Optional[Expression] = default_field(func=pc.ceil)
461
- floor: Optional[Expression] = default_field(func=pc.floor)
462
- trunc: Optional[Expression] = default_field(func=pc.trunc)
463
+ ceil: Expression | None = default_field(func=pc.ceil)
464
+ floor: Expression | None = default_field(func=pc.floor)
465
+ trunc: Expression | None = default_field(func=pc.trunc)
463
466
 
464
- round: Optional[Expression] = default_field(func=pc.round)
467
+ round: Expression | None = default_field(func=pc.round)
465
468
  ndigits: int = 0
466
469
  round_mode: str = 'half_to_even'
467
470
  multiple: float = 1.0
@@ -474,8 +477,8 @@ class Rounding(Fields):
474
477
 
475
478
  @strawberry.input(description="Logarithmic functions.")
476
479
  class Log(Fields):
477
- ln: Optional[Expression] = default_field(func=pc.ln)
478
- log1p: Optional[Expression] = default_field(func=pc.log1p)
480
+ ln: Expression | None = default_field(func=pc.ln)
481
+ log1p: Expression | None = default_field(func=pc.log1p)
479
482
  logb: list[Expression] = default_field([], func=pc.logb)
480
483
 
481
484
 
@@ -483,13 +486,13 @@ class Log(Fields):
483
486
  class Trig(Fields):
484
487
  checked: bool = strawberry.field(default=False, description="check for overflow errors")
485
488
 
486
- acos: Optional[Expression] = default_field(func=pc.acos)
487
- asin: Optional[Expression] = default_field(func=pc.asin)
488
- atan: Optional[Expression] = default_field(func=pc.atan)
489
+ acos: Expression | None = default_field(func=pc.acos)
490
+ asin: Expression | None = default_field(func=pc.asin)
491
+ atan: Expression | None = default_field(func=pc.atan)
489
492
  atan2: list[Expression] = default_field([], func=pc.atan2)
490
- cos: Optional[Expression] = default_field(func=pc.cos)
491
- sin: Optional[Expression] = default_field(func=pc.sin)
492
- tan: Optional[Expression] = default_field(func=pc.tan)
493
+ cos: Expression | None = default_field(func=pc.cos)
494
+ sin: Expression | None = default_field(func=pc.sin)
495
+ tan: Expression | None = default_field(func=pc.tan)
493
496
 
494
497
  def getfunc(self, name):
495
498
  return getattr(pc, name + ('_checked' * self.checked))
@@ -504,40 +507,40 @@ class ElementWise(Fields):
504
507
 
505
508
  @strawberry.input(description="Utf8 string functions.")
506
509
  class Utf8(Fields):
507
- is_alnum: Optional[Expression] = default_field(func=pc.utf8_is_alnum)
508
- is_alpha: Optional[Expression] = default_field(func=pc.utf8_is_alpha)
509
- is_decimal: Optional[Expression] = default_field(func=pc.utf8_is_decimal)
510
- is_digit: Optional[Expression] = default_field(func=pc.utf8_is_digit)
511
- is_lower: Optional[Expression] = default_field(func=pc.utf8_is_lower)
512
- is_numeric: Optional[Expression] = default_field(func=pc.utf8_is_numeric)
513
- is_printable: Optional[Expression] = default_field(func=pc.utf8_is_printable)
514
- is_space: Optional[Expression] = default_field(func=pc.utf8_is_space)
515
- is_title: Optional[Expression] = default_field(func=pc.utf8_is_title)
516
- is_upper: Optional[Expression] = default_field(func=pc.utf8_is_upper)
517
-
518
- capitalize: Optional[Expression] = default_field(func=pc.utf8_capitalize)
519
- length: Optional[Expression] = default_field(func=pc.utf8_length)
520
- lower: Optional[Expression] = default_field(func=pc.utf8_lower)
521
- reverse: Optional[Expression] = default_field(func=pc.utf8_reverse)
522
- swapcase: Optional[Expression] = default_field(func=pc.utf8_swapcase)
523
- title: Optional[Expression] = default_field(func=pc.utf8_title)
524
- upper: Optional[Expression] = default_field(func=pc.utf8_upper)
525
-
526
- ltrim: Optional[Expression] = default_field(func=pc.utf8_ltrim)
527
- rtrim: Optional[Expression] = default_field(func=pc.utf8_rtrim)
528
- trim: Optional[Expression] = default_field(func=pc.utf8_trim)
510
+ is_alnum: Expression | None = default_field(func=pc.utf8_is_alnum)
511
+ is_alpha: Expression | None = default_field(func=pc.utf8_is_alpha)
512
+ is_decimal: Expression | None = default_field(func=pc.utf8_is_decimal)
513
+ is_digit: Expression | None = default_field(func=pc.utf8_is_digit)
514
+ is_lower: Expression | None = default_field(func=pc.utf8_is_lower)
515
+ is_numeric: Expression | None = default_field(func=pc.utf8_is_numeric)
516
+ is_printable: Expression | None = default_field(func=pc.utf8_is_printable)
517
+ is_space: Expression | None = default_field(func=pc.utf8_is_space)
518
+ is_title: Expression | None = default_field(func=pc.utf8_is_title)
519
+ is_upper: Expression | None = default_field(func=pc.utf8_is_upper)
520
+
521
+ capitalize: Expression | None = default_field(func=pc.utf8_capitalize)
522
+ length: Expression | None = default_field(func=pc.utf8_length)
523
+ lower: Expression | None = default_field(func=pc.utf8_lower)
524
+ reverse: Expression | None = default_field(func=pc.utf8_reverse)
525
+ swapcase: Expression | None = default_field(func=pc.utf8_swapcase)
526
+ title: Expression | None = default_field(func=pc.utf8_title)
527
+ upper: Expression | None = default_field(func=pc.utf8_upper)
528
+
529
+ ltrim: Expression | None = default_field(func=pc.utf8_ltrim)
530
+ rtrim: Expression | None = default_field(func=pc.utf8_rtrim)
531
+ trim: Expression | None = default_field(func=pc.utf8_trim)
529
532
  characters: str = default_field('', description="trim options; by default trims whitespace")
530
533
 
531
- replace_slice: Optional[Expression] = default_field(func=pc.utf8_replace_slice)
532
- slice_codeunits: Optional[Expression] = default_field(func=pc.utf8_slice_codeunits)
534
+ replace_slice: Expression | None = default_field(func=pc.utf8_replace_slice)
535
+ slice_codeunits: Expression | None = default_field(func=pc.utf8_slice_codeunits)
533
536
  start: int = 0
534
- stop: Optional[int] = UNSET
537
+ stop: int | None = UNSET
535
538
  step: int = 1
536
539
  replacement: str = ''
537
540
 
538
- center: Optional[Expression] = default_field(func=pc.utf8_center)
539
- lpad: Optional[Expression] = default_field(func=pc.utf8_lpad)
540
- rpad: Optional[Expression] = default_field(func=pc.utf8_rpad)
541
+ center: Expression | None = default_field(func=pc.utf8_center)
542
+ lpad: Expression | None = default_field(func=pc.utf8_lpad)
543
+ rpad: Expression | None = default_field(func=pc.utf8_rpad)
541
544
  width: int = 0
542
545
  padding: str = ''
543
546
 
@@ -549,16 +552,16 @@ class Utf8(Fields):
549
552
 
550
553
  @strawberry.input(description="Binary functions.")
551
554
  class Binary(Fields):
552
- length: Optional[Expression] = default_field(func=pc.binary_length)
555
+ length: Expression | None = default_field(func=pc.binary_length)
553
556
  repeat: list[Expression] = default_field([], func=pc.binary_repeat)
554
- reverse: Optional[Expression] = default_field(func=pc.binary_reverse)
557
+ reverse: Expression | None = default_field(func=pc.binary_reverse)
555
558
 
556
559
  join: list[Expression] = default_field([], func=pc.binary_join)
557
560
  join_element_wise: list[Expression] = default_field([], func=pc.binary_join_element_wise)
558
561
  null_handling: str = 'emit_null'
559
562
  null_replacement: str = ''
560
563
 
561
- replace_slice: Optional[Expression] = default_field(func=pc.binary_replace_slice)
564
+ replace_slice: Expression | None = default_field(func=pc.binary_replace_slice)
562
565
  start: int = 0
563
566
  stop: int = 0
564
567
  replacement: bytes = b''
@@ -568,22 +571,20 @@ class Binary(Fields):
568
571
 
569
572
  @strawberry.input(description="Match substring functions.")
570
573
  class MatchSubstring(Fields):
571
- count_substring: Optional[Expression] = default_field(name='count', func=pc.count_substring)
572
- ends_with: Optional[Expression] = default_field(func=pc.ends_with)
573
- find_substring: Optional[Expression] = default_field(name='find', func=pc.find_substring)
574
- match_substring: Optional[Expression] = default_field(name='match', func=pc.match_substring)
575
- starts_with: Optional[Expression] = default_field(func=pc.starts_with)
576
- replace_substring: Optional[Expression] = default_field(
577
- name='replace', func=pc.replace_substring
578
- )
579
- split_pattern: Optional[Expression] = default_field(name='split', func=pc.split_pattern)
580
- extract: Optional[Expression] = default_field(func=pc.extract_regex)
574
+ count_substring: Expression | None = default_field(name='count', func=pc.count_substring)
575
+ ends_with: Expression | None = default_field(func=pc.ends_with)
576
+ find_substring: Expression | None = default_field(name='find', func=pc.find_substring)
577
+ match_substring: Expression | None = default_field(name='match', func=pc.match_substring)
578
+ starts_with: Expression | None = default_field(func=pc.starts_with)
579
+ replace_substring: Expression | None = default_field(name='replace', func=pc.replace_substring)
580
+ split_pattern: Expression | None = default_field(name='split', func=pc.split_pattern)
581
+ extract: Expression | None = default_field(func=pc.extract_regex)
581
582
  pattern: str = ''
582
583
  ignore_case: bool = False
583
584
  regex: bool = False
584
585
  replacement: str = ''
585
- max_replacements: Optional[int] = None
586
- max_splits: Optional[int] = None
586
+ max_replacements: int | None = None
587
+ max_splits: int | None = None
587
588
  reverse: bool = False
588
589
 
589
590
  def getfunc(self, name):
@@ -594,26 +595,26 @@ class MatchSubstring(Fields):
594
595
 
595
596
  @strawberry.input(description="Temporal functions.")
596
597
  class Temporal(Fields):
597
- day: Optional[Expression] = default_field(func=pc.day)
598
- day_of_year: Optional[Expression] = default_field(func=pc.day_of_year)
599
- hour: Optional[Expression] = default_field(func=pc.hour)
600
- iso_week: Optional[Expression] = default_field(func=pc.iso_week)
601
- iso_year: Optional[Expression] = default_field(func=pc.iso_year)
602
- iso_calendar: Optional[Expression] = default_field(func=pc.iso_calendar)
603
- is_leap_year: Optional[Expression] = default_field(func=pc.is_leap_year)
604
-
605
- microsecond: Optional[Expression] = default_field(func=pc.microsecond)
606
- millisecond: Optional[Expression] = default_field(func=pc.millisecond)
607
- minute: Optional[Expression] = default_field(func=pc.minute)
608
- month: Optional[Expression] = default_field(func=pc.month)
609
- nanosecond: Optional[Expression] = default_field(func=pc.nanosecond)
610
- quarter: Optional[Expression] = default_field(func=pc.quarter)
611
- second: Optional[Expression] = default_field(func=pc.second)
612
- subsecond: Optional[Expression] = default_field(func=pc.subsecond)
613
- us_week: Optional[Expression] = default_field(func=pc.us_week)
614
- us_year: Optional[Expression] = default_field(func=pc.us_year)
615
- year: Optional[Expression] = default_field(func=pc.year)
616
- year_month_day: Optional[Expression] = default_field(func=pc.year_month_day)
598
+ day: Expression | None = default_field(func=pc.day)
599
+ day_of_year: Expression | None = default_field(func=pc.day_of_year)
600
+ hour: Expression | None = default_field(func=pc.hour)
601
+ iso_week: Expression | None = default_field(func=pc.iso_week)
602
+ iso_year: Expression | None = default_field(func=pc.iso_year)
603
+ iso_calendar: Expression | None = default_field(func=pc.iso_calendar)
604
+ is_leap_year: Expression | None = default_field(func=pc.is_leap_year)
605
+
606
+ microsecond: Expression | None = default_field(func=pc.microsecond)
607
+ millisecond: Expression | None = default_field(func=pc.millisecond)
608
+ minute: Expression | None = default_field(func=pc.minute)
609
+ month: Expression | None = default_field(func=pc.month)
610
+ nanosecond: Expression | None = default_field(func=pc.nanosecond)
611
+ quarter: Expression | None = default_field(func=pc.quarter)
612
+ second: Expression | None = default_field(func=pc.second)
613
+ subsecond: Expression | None = default_field(func=pc.subsecond)
614
+ us_week: Expression | None = default_field(func=pc.us_week)
615
+ us_year: Expression | None = default_field(func=pc.us_year)
616
+ year: Expression | None = default_field(func=pc.year)
617
+ year_month_day: Expression | None = default_field(func=pc.year_month_day)
617
618
 
618
619
  day_time_interval_between: list[Expression] = default_field(
619
620
  [], func=pc.day_time_interval_between
@@ -633,29 +634,29 @@ class Temporal(Fields):
633
634
  weeks_between: list[Expression] = default_field([], func=pc.weeks_between)
634
635
  years_between: list[Expression] = default_field([], func=pc.years_between)
635
636
 
636
- ceil_temporal: Optional[Expression] = default_field(name='ceil', func=pc.ceil_temporal)
637
- floor_temporal: Optional[Expression] = default_field(name='floor', func=pc.floor_temporal)
638
- round_temporal: Optional[Expression] = default_field(name='round', func=pc.round_temporal)
637
+ ceil_temporal: Expression | None = default_field(name='ceil', func=pc.ceil_temporal)
638
+ floor_temporal: Expression | None = default_field(name='floor', func=pc.floor_temporal)
639
+ round_temporal: Expression | None = default_field(name='round', func=pc.round_temporal)
639
640
  multiple: int = 1
640
641
  unit: str = 'day'
641
642
  week_starts_monday: bool = True
642
643
  ceil_is_strictly_greater: bool = False
643
644
  calendar_based_origin: bool = False
644
645
 
645
- week: Optional[Expression] = default_field(func=pc.week)
646
- count_from_zero: Optional[bool] = UNSET
646
+ week: Expression | None = default_field(func=pc.week)
647
+ count_from_zero: bool | None = UNSET
647
648
  first_week_is_fully_in_year: bool = False
648
649
 
649
- day_of_week: Optional[Expression] = default_field(func=pc.day_of_week)
650
+ day_of_week: Expression | None = default_field(func=pc.day_of_week)
650
651
  week_start: int = 1
651
652
 
652
- strftime: Optional[Expression] = default_field(func=pc.strftime)
653
- strptime: Optional[Expression] = default_field(func=pc.strptime)
653
+ strftime: Expression | None = default_field(func=pc.strftime)
654
+ strptime: Expression | None = default_field(func=pc.strptime)
654
655
  format: str = '%Y-%m-%dT%H:%M:%S'
655
656
  locale: str = 'C'
656
657
  error_is_null: bool = False
657
658
 
658
- assume_timezone: Optional[Expression] = default_field(func=pc.assume_timezone)
659
+ assume_timezone: Expression | None = default_field(func=pc.assume_timezone)
659
660
  timezone: str = ''
660
661
  ambiguous: str = 'raise'
661
662
  nonexistent: str = 'raise'
@@ -684,16 +685,16 @@ class SetLookup(Fields):
684
685
  @strawberry.input(description="List array functions.")
685
686
  class Lists(Fields):
686
687
  element: list[Expression] = default_field([], func=pc.list_element)
687
- value_length: Optional[Expression] = default_field(func=pc.list_value_length)
688
+ value_length: Expression | None = default_field(func=pc.list_value_length)
688
689
  # user defined functions
689
- all: Optional[Expression] = default_field(func=pc.all)
690
- any: Optional[Expression] = default_field(func=pc.any)
690
+ all: Expression | None = default_field(func=pc.all)
691
+ any: Expression | None = default_field(func=pc.any)
691
692
 
692
- slice: Optional[Expression] = default_field(func=pc.list_slice)
693
+ slice: Expression | None = default_field(func=pc.list_slice)
693
694
  start: int = 0
694
- stop: Optional[int] = None
695
+ stop: int | None = None
695
696
  step: int = 1
696
- return_fixed_size_list: Optional[bool] = None
697
+ return_fixed_size_list: bool | None = None
697
698
 
698
699
  prefix = 'list_'
699
700
 
@@ -10,7 +10,7 @@ import inspect
10
10
  import itertools
11
11
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sized
12
12
  from datetime import timedelta
13
- from typing import Annotated, Optional, Union, no_type_check
13
+ from typing import Annotated, TypeAlias, no_type_check
14
14
  import pyarrow as pa
15
15
  import pyarrow.compute as pc
16
16
  import pyarrow.dataset as ds
@@ -20,12 +20,12 @@ from strawberry.extensions.utils import get_path_from_info
20
20
  from typing_extensions import Self
21
21
  from .core import Agg, Batch, Column as C, ListChunk, Nodes, Table as T
22
22
  from .inputs import CountAggregate, Cumulative, Diff, Expression, Field, Filter
23
- from .inputs import HashAggregates, ListFunction, Pairwise, Projection, Rank
23
+ from .inputs import HashAggregates, ListFunction, Pairwise, Projection, Rank, RankQuantile
24
24
  from .inputs import ScalarAggregate, TDigestAggregate, VarianceAggregate, links, provisional
25
25
  from .models import Column, doc_field
26
26
  from .scalars import Long
27
27
 
28
- Source = Union[ds.Dataset, Nodes, ds.Scanner, pa.Table]
28
+ Source: TypeAlias = ds.Dataset | Nodes | ds.Scanner | pa.Table
29
29
 
30
30
 
31
31
  def references(field) -> Iterator:
@@ -83,7 +83,7 @@ class Dataset:
83
83
  return self.source.select(names)
84
84
  return Nodes.scan(self.source, names)
85
85
 
86
- def to_table(self, info: Info, length: Optional[int] = None) -> pa.Table:
86
+ def to_table(self, info: Info, length: int | None = None) -> pa.Table:
87
87
  """Return table with only the rows and columns necessary to proceed."""
88
88
  source = self.select(info)
89
89
  if isinstance(source, pa.Table):
@@ -163,7 +163,7 @@ class Dataset:
163
163
  ) # type: ignore
164
164
 
165
165
  @doc_field
166
- def optional(self) -> Optional[Self]:
166
+ def optional(self) -> Self | None:
167
167
  """Nullable field to stop error propagation, enabling partial query results.
168
168
 
169
169
  Will be replaced by client controlled nullability.
@@ -192,7 +192,7 @@ class Dataset:
192
192
  return len(table) >= length
193
193
 
194
194
  @doc_field
195
- def size(self) -> Optional[Long]:
195
+ def size(self) -> Long | None:
196
196
  """buffer size in bytes; null if table is not loaded"""
197
197
  return getattr(self.source, 'nbytes', None)
198
198
 
@@ -203,7 +203,7 @@ class Dataset:
203
203
  )
204
204
  def column(
205
205
  self, info: Info, name: list[str], cast: str = '', safe: bool = True
206
- ) -> Optional[Column]:
206
+ ) -> Column | None:
207
207
  """Return column of any type by name.
208
208
 
209
209
  This is typically only needed for aliased or casted columns.
@@ -222,14 +222,14 @@ class Dataset:
222
222
  reverse="reverse order after slicing; forces a copy",
223
223
  )
224
224
  def slice(
225
- self, info: Info, offset: Long = 0, length: Optional[Long] = None, reverse: bool = False
225
+ self, info: Info, offset: Long = 0, length: Long | None = None, reverse: bool = False
226
226
  ) -> Self:
227
227
  """Return zero-copy slice of table.
228
228
 
229
- Can also be sued to force loading a dataset.
229
+ Can also be used to force loading a dataset.
230
230
  """
231
231
  table = self.to_table(info, length and (offset + length if offset >= 0 else None))
232
- table = table[offset:][:length] # `slice` bug: ARROW-15412
232
+ table = table[offset:][:length] # `slice` bug: apache/arrow#30894
233
233
  return type(self)(table[::-1] if reverse else table)
234
234
 
235
235
  @doc_field(
@@ -301,7 +301,7 @@ class Dataset:
301
301
  self,
302
302
  info: Info,
303
303
  by: list[str],
304
- length: Optional[Long] = None,
304
+ length: Long | None = None,
305
305
  null_placement: str = 'at_end',
306
306
  ) -> Self:
307
307
  """Return table slice sorted by specified columns.
@@ -362,6 +362,8 @@ class Dataset:
362
362
  fill_null_forward: doc_argument(list[Field], func=pc.fill_null_forward) = [],
363
363
  pairwise_diff: doc_argument(list[Pairwise], func=pc.pairwise_diff) = [],
364
364
  rank: doc_argument(list[Rank], func=pc.rank) = [],
365
+ rank_quantile: doc_argument(list[RankQuantile], func=pc.rank_quantile) = [],
366
+ rank_normal: doc_argument(list[RankQuantile], func=pc.rank_normal) = [],
365
367
  list_: Annotated[
366
368
  ListFunction,
367
369
  strawberry.argument(name='list', description="functions for list arrays."),
@@ -377,7 +379,7 @@ class Dataset:
377
379
  columns = {}
378
380
  funcs = pc.cumulative_max, pc.cumulative_mean, pc.cumulative_min, pc.cumulative_prod
379
381
  funcs += pc.cumulative_sum, C.fill_null_backward, C.fill_null_forward, C.pairwise_diff
380
- funcs += (pc.rank,)
382
+ funcs += pc.rank, pc.rank_quantile, pc.rank_normal
381
383
  for func in funcs:
382
384
  for field in locals()[func.__name__]:
383
385
  callable = func
@@ -396,7 +398,7 @@ class Dataset:
396
398
  return type(self)(self.add_metric(info, table, mode='batch'))
397
399
 
398
400
  @doc_field
399
- def tables(self, info: Info) -> list[Optional[Self]]: # type: ignore
401
+ def tables(self, info: Info) -> list[Self | None]: # type: ignore
400
402
  """Return a list of tables by splitting list columns.
401
403
 
402
404
  At least one list column must be referenced, and all list columns must have the same lengths.
@@ -477,7 +479,7 @@ class Dataset:
477
479
  info: Info,
478
480
  right: str,
479
481
  keys: list[str],
480
- right_keys: Optional[list[str]] = None,
482
+ right_keys: list[str] | None = None,
481
483
  join_type: str = 'left outer',
482
484
  left_suffix: str = '',
483
485
  right_suffix: str = '',
@@ -6,7 +6,6 @@ import warnings
6
6
  from collections.abc import Iterable, Mapping
7
7
  from datetime import timedelta
8
8
  from keyword import iskeyword
9
- from typing import Optional
10
9
  import pyarrow.dataset as ds
11
10
  import strawberry.asgi
12
11
  from strawberry import Info, UNSET
@@ -33,7 +32,7 @@ class MetricsExtension(tracing.ApolloTracingExtension):
33
32
  return {'metrics': metrics}
34
33
 
35
34
  @staticmethod
36
- def duration(data: dict) -> Optional[str]:
35
+ def duration(data: dict) -> str | None:
37
36
  return data['duration'] and str(timedelta(microseconds=data['duration'] / 1e3))
38
37
 
39
38
 
@@ -87,12 +86,12 @@ def implemented(root: Source, name: str = '', keys: Iterable = ()):
87
86
  prefix = to_camel_case(name.title())
88
87
 
89
88
  namespace = {name: strawberry.field(default=UNSET, name=name) for name in types}
90
- annotations = {name: Optional[Column.registry[types[name]]] for name in types}
89
+ annotations = {name: Column.registry[types[name]] | None for name in types}
91
90
  cls = type(prefix + 'Columns', (), dict(namespace, __annotations__=annotations))
92
91
  Columns = strawberry.type(cls, description="fields for each column")
93
92
 
94
93
  namespace = {name: strawberry.field(default=UNSET, name=name) for name in types}
95
- annotations = {name: Optional[Column if cls is list else cls] for name, cls in types.items()}
94
+ annotations = {name: (Column if cls is list else cls) | None for name, cls in types.items()}
96
95
  cls = type(prefix + 'Row', (), dict(namespace, __annotations__=annotations))
97
96
  Row = strawberry.type(cls, description="scalar fields")
98
97
 
@@ -104,7 +103,7 @@ def implemented(root: Source, name: str = '', keys: Iterable = ()):
104
103
  """fields for each column"""
105
104
  return Columns(**super().columns(info))
106
105
 
107
- def row(self, info: Info, index: Long = 0) -> Optional[Row]: # type: ignore
106
+ def row(self, info: Info, index: Long = 0) -> Row | None: # type: ignore
108
107
  """Return scalar values at index."""
109
108
  row = super().row(info, index)
110
109
  for name, value in row.items():
@@ -2,12 +2,13 @@
2
2
  GraphQL output types and resolvers.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  import functools
6
7
  import inspect
7
8
  from collections.abc import Callable
8
9
  from datetime import date, datetime, time, timedelta
9
10
  from decimal import Decimal
10
- from typing import Annotated, Generic, Optional, TypeVar, TYPE_CHECKING, get_args
11
+ from typing import Annotated, Generic, TypeVar, TYPE_CHECKING, get_args
11
12
  import pyarrow as pa
12
13
  import pyarrow.compute as pc
13
14
  import strawberry
@@ -27,7 +28,7 @@ def selections(*fields) -> set:
27
28
  return {selection.name for field in fields for selection in field.selections}
28
29
 
29
30
 
30
- def doc_field(func: Optional[Callable] = None, **kwargs: str) -> StrawberryField:
31
+ def doc_field(func: Callable | None = None, **kwargs: str) -> StrawberryField:
31
32
  """Return strawberry field with argument and docstring descriptions."""
32
33
  if func is None:
33
34
  return functools.partial(doc_field, **kwargs) # type: ignore
@@ -77,12 +78,12 @@ class Column:
77
78
  return self.array.nbytes
78
79
 
79
80
  @classmethod
80
- def cast(cls, array: pa.ChunkedArray) -> 'Column':
81
+ def cast(cls, array: pa.ChunkedArray) -> Column:
81
82
  """Return typed column based on array type."""
82
83
  return cls.registry[py_type(array.type)](array)
83
84
 
84
85
  @classmethod
85
- def fromscalar(cls, scalar: pa.ListScalar) -> Optional['Column']:
86
+ def fromscalar(cls, scalar: pa.ListScalar) -> Column | None:
86
87
  return None if scalar.values is None else cls.cast(pa.chunked_array([scalar.values]))
87
88
 
88
89
  @compute_field
@@ -105,7 +106,7 @@ class Set(Generic[T]):
105
106
  self.array, self.counts = array, counts.to_pylist()
106
107
 
107
108
  @doc_field
108
- def values(self) -> list[Optional[T]]:
109
+ def values(self) -> list[T | None]:
109
110
  """list of values"""
110
111
  return self.array.to_pylist()
111
112
 
@@ -126,7 +127,7 @@ class NominalColumn(Generic[T], Column):
126
127
  return Set(self.array.unique())
127
128
 
128
129
  @doc_field
129
- def value(self, index: Long = 0) -> Optional[T]:
130
+ def value(self, index: Long = 0) -> T | None:
130
131
  """scalar value at index"""
131
132
  return self.array[index].as_py()
132
133
 
@@ -139,23 +140,23 @@ class NominalColumn(Generic[T], Column):
139
140
  @strawberry.type(name='Column', description="column of ordinal values")
140
141
  class OrdinalColumn(NominalColumn[T]):
141
142
  @compute_field
142
- def first(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
143
+ def first(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
143
144
  return pc.first(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
144
145
 
145
146
  @compute_field
146
- def last(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
147
+ def last(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
147
148
  return pc.last(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
148
149
 
149
150
  @compute_field
150
- def min(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
151
+ def min(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
151
152
  return pc.min(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
152
153
 
153
154
  @compute_field
154
- def max(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
155
+ def max(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
155
156
  return pc.max(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
156
157
 
157
158
  @compute_field
158
- def index(self, value: T, start: Long = 0, end: Optional[Long] = None) -> Long:
159
+ def index(self, value: T, start: Long = 0, end: Long | None = None) -> Long:
159
160
  return C.index(self.array, value, start, end)
160
161
 
161
162
  @compute_field
@@ -175,15 +176,15 @@ class IntervalColumn(OrdinalColumn[T]):
175
176
  return Set(*pc.mode(self.array, n, skip_nulls=skip_nulls, min_count=min_count).flatten())
176
177
 
177
178
  @compute_field
178
- def sum(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
179
+ def sum(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
179
180
  return pc.sum(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
180
181
 
181
182
  @compute_field
182
- def product(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[T]:
183
+ def product(self, skip_nulls: bool = True, min_count: int = 0) -> T | None:
183
184
  return pc.product(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
184
185
 
185
186
  @compute_field
186
- def mean(self, skip_nulls: bool = True, min_count: int = 0) -> Optional[float]:
187
+ def mean(self, skip_nulls: bool = True, min_count: int = 0) -> float | None:
187
188
  return pc.mean(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
188
189
 
189
190
  @compute_field
@@ -195,13 +196,11 @@ class IntervalColumn(OrdinalColumn[T]):
195
196
  @strawberry.type(name='Column', description="column of floats or decimals")
196
197
  class RatioColumn(IntervalColumn[T]):
197
198
  @compute_field
198
- def stddev(self, ddof: int = 0, skip_nulls: bool = True, min_count: int = 0) -> Optional[float]:
199
+ def stddev(self, ddof: int = 0, skip_nulls: bool = True, min_count: int = 0) -> float | None:
199
200
  return pc.stddev(self.array, ddof=ddof, skip_nulls=skip_nulls, min_count=min_count).as_py()
200
201
 
201
202
  @compute_field
202
- def variance(
203
- self, ddof: int = 0, skip_nulls: bool = True, min_count: int = 0
204
- ) -> Optional[float]:
203
+ def variance(self, ddof: int = 0, skip_nulls: bool = True, min_count: int = 0) -> float | None:
205
204
  options = {'skip_nulls': skip_nulls, 'min_count': min_count}
206
205
  return pc.variance(self.array, ddof=ddof, **options).as_py()
207
206
 
@@ -212,7 +211,7 @@ class RatioColumn(IntervalColumn[T]):
212
211
  interpolation: str = 'linear',
213
212
  skip_nulls: bool = True,
214
213
  min_count: int = 0,
215
- ) -> list[Optional[float]]:
214
+ ) -> list[float | None]:
216
215
  options = {'skip_nulls': skip_nulls, 'min_count': min_count}
217
216
  return pc.quantile(self.array, q=q, interpolation=interpolation, **options).to_pylist()
218
217
 
@@ -224,7 +223,7 @@ class RatioColumn(IntervalColumn[T]):
224
223
  buffer_size: int = 500,
225
224
  skip_nulls: bool = True,
226
225
  min_count: int = 0,
227
- ) -> list[Optional[float]]:
226
+ ) -> list[float | None]:
228
227
  options = {'buffer_size': buffer_size, 'skip_nulls': skip_nulls, 'min_count': min_count}
229
228
  return pc.tdigest(self.array, q=q, delta=delta, **options).to_pylist()
230
229
 
@@ -233,11 +232,11 @@ class RatioColumn(IntervalColumn[T]):
233
232
  @strawberry.type(name='eanColumn', description="column of booleans")
234
233
  class BooleanColumn(IntervalColumn[T]):
235
234
  @compute_field
236
- def any(self, skip_nulls: bool = True, min_count: int = 1) -> Optional[bool]:
235
+ def any(self, skip_nulls: bool = True, min_count: int = 1) -> bool | None:
237
236
  return pc.any(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
238
237
 
239
238
  @compute_field
240
- def all(self, skip_nulls: bool = True, min_count: int = 1) -> Optional[bool]:
239
+ def all(self, skip_nulls: bool = True, min_count: int = 1) -> bool | None:
241
240
  return pc.all(self.array, skip_nulls=skip_nulls, min_count=min_count).as_py()
242
241
 
243
242
 
@@ -247,7 +246,7 @@ class IntColumn(RatioColumn[T]):
247
246
  @doc_field
248
247
  def take_from(
249
248
  self, info: Info, field: str
250
- ) -> Optional[Annotated['Dataset', strawberry.lazy('.interface')]]:
249
+ ) -> Annotated['Dataset', strawberry.lazy('.interface')] | None:
251
250
  """Select indices from a table on the root Query type."""
252
251
  root = getattr(info.root_value, field)
253
252
  return type(root)(root.select(info).take(self.array.combine_chunks()))
@@ -257,12 +256,12 @@ class IntColumn(RatioColumn[T]):
257
256
  @strawberry.type(description="column of lists")
258
257
  class ListColumn(Column):
259
258
  @doc_field
260
- def value(self, index: Long = 0) -> Optional[Column]:
259
+ def value(self, index: Long = 0) -> Column | None:
261
260
  """scalar column at index"""
262
261
  return self.fromscalar(self.array[index])
263
262
 
264
263
  @doc_field
265
- def values(self) -> list[Optional[Column]]:
264
+ def values(self) -> list[Column | None]:
266
265
  """list of columns"""
267
266
  return list(map(self.fromscalar, self.array))
268
267
 
@@ -280,7 +279,7 @@ class ListColumn(Column):
280
279
  @strawberry.type(description="column of structs")
281
280
  class StructColumn(Column):
282
281
  @doc_field
283
- def value(self, index: Long = 0) -> Optional[dict]:
282
+ def value(self, index: Long = 0) -> dict | None:
284
283
  """scalar json object at index"""
285
284
  return self.array[index].as_py()
286
285
 
@@ -290,6 +289,6 @@ class StructColumn(Column):
290
289
  return [field.name for field in self.array.type]
291
290
 
292
291
  @doc_field(name="field name(s); multiple names access nested fields")
293
- def column(self, name: list[str]) -> Optional[Column]:
292
+ def column(self, name: list[str]) -> Column | None:
294
293
  """Return struct field as a column."""
295
294
  return self.cast(pc.struct_field(self.array, name))
@@ -5,7 +5,6 @@ GraphQL scalars.
5
5
  import functools
6
6
  from datetime import date, datetime, time, timedelta
7
7
  from decimal import Decimal
8
- from typing import Union
9
8
  import isodate
10
9
  import pyarrow as pa
11
10
  import strawberry
@@ -39,7 +38,7 @@ def _(mdn: pa.MonthDayNano) -> str:
39
38
 
40
39
  Long = strawberry.scalar(int, name='Long', description="64-bit int", parse_value=parse_long)
41
40
  Duration = strawberry.scalar(
42
- Union[timedelta, pa.MonthDayNano],
41
+ timedelta | pa.MonthDayNano,
43
42
  name='Duration',
44
43
  description="Duration (isoformat)",
45
44
  specified_by_url="https://en.wikipedia.org/wiki/ISO_8601#Durations",
@@ -66,6 +65,8 @@ type_map = {
66
65
  pa.lib.Type_HALF_FLOAT: float,
67
66
  pa.lib.Type_FLOAT: float,
68
67
  pa.lib.Type_DOUBLE: float,
68
+ pa.lib.Type_DECIMAL32: Decimal,
69
+ pa.lib.Type_DECIMAL64: Decimal,
69
70
  pa.lib.Type_DECIMAL128: Decimal,
70
71
  pa.lib.Type_DECIMAL256: Decimal,
71
72
  pa.lib.Type_DATE32: date,
@@ -12,7 +12,7 @@ progress on the second pass.
12
12
  import operator
13
13
  import shutil
14
14
  from pathlib import Path
15
- from typing import Annotated, Callable, Optional
15
+ from typing import Annotated, Callable
16
16
  import numpy as np
17
17
  import pyarrow.compute as pc
18
18
  import pyarrow.dataset as ds
@@ -43,7 +43,7 @@ def write_batches(
43
43
  pbar.update(len(batch))
44
44
 
45
45
 
46
- def write_fragments(dataset: ds.Dataset, base_dir: str, func: Optional[Callable] = None, **options):
46
+ def write_fragments(dataset: ds.Dataset, base_dir: str, func: Callable | None = None, **options):
47
47
  """Rewrite partition files by fragment to consolidate, optionally transforming."""
48
48
  options['format'] = 'parquet'
49
49
  exprs = {Path(frag.path).parent: frag.partition_expression for frag in dataset.get_fragments()}
@@ -1,22 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: graphique
3
- Version: 1.8
3
+ Version: 1.9
4
4
  Summary: GraphQL service for arrow tables and parquet data sets.
5
5
  Author-email: Aric Coady <aric.coady@gmail.com>
6
- License: Copyright 2022 Aric Coady
7
-
8
- Licensed under the Apache License, Version 2.0 (the "License");
9
- you may not use this file except in compliance with the License.
10
- You may obtain a copy of the License at
11
-
12
- http://www.apache.org/licenses/LICENSE-2.0
13
-
14
- Unless required by applicable law or agreed to in writing, software
15
- distributed under the License is distributed on an "AS IS" BASIS,
16
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- See the License for the specific language governing permissions and
18
- limitations under the License.
19
-
6
+ License: Apache-2.0
20
7
  Project-URL: Homepage, https://github.com/coady/graphique
21
8
  Project-URL: Documentation, https://coady.github.io/graphique
22
9
  Project-URL: Changelog, https://github.com/coady/graphique/blob/main/CHANGELOG.md
@@ -25,10 +12,8 @@ Keywords: graphql,arrow,parquet
25
12
  Classifier: Development Status :: 5 - Production/Stable
26
13
  Classifier: Intended Audience :: Developers
27
14
  Classifier: Intended Audience :: Science/Research
28
- Classifier: License :: OSI Approved :: Apache Software License
29
15
  Classifier: Operating System :: OS Independent
30
16
  Classifier: Programming Language :: Python :: 3
31
- Classifier: Programming Language :: Python :: 3.9
32
17
  Classifier: Programming Language :: Python :: 3.10
33
18
  Classifier: Programming Language :: Python :: 3.11
34
19
  Classifier: Programming Language :: Python :: 3.12
@@ -38,10 +23,10 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
38
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
39
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
25
  Classifier: Typing :: Typed
41
- Requires-Python: >=3.9
26
+ Requires-Python: >=3.10
42
27
  Description-Content-Type: text/markdown
43
28
  License-File: LICENSE.txt
44
- Requires-Dist: pyarrow>=18
29
+ Requires-Dist: pyarrow>=20
45
30
  Requires-Dist: strawberry-graphql[asgi,cli]>=0.236
46
31
  Requires-Dist: numpy
47
32
  Requires-Dist: isodate>=0.7
@@ -49,6 +34,7 @@ Provides-Extra: server
49
34
  Requires-Dist: uvicorn[standard]; extra == "server"
50
35
  Provides-Extra: cli
51
36
  Requires-Dist: tqdm; extra == "cli"
37
+ Dynamic: license-file
52
38
 
53
39
  [![image](https://img.shields.io/pypi/v/graphique.svg)](https://pypi.org/project/graphique/)
54
40
  ![image](https://img.shields.io/pypi/pyversions/graphique.svg)
@@ -1,4 +1,4 @@
1
- pyarrow>=18
1
+ pyarrow>=20
2
2
  strawberry-graphql[asgi,cli]>=0.236
3
3
  numpy
4
4
  isodate>=0.7
@@ -4,22 +4,20 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphique"
7
- version = "1.8"
7
+ version = "1.9"
8
8
  dynamic = ["dependencies"]
9
9
  description = "GraphQL service for arrow tables and parquet data sets."
10
10
  readme = "README.md"
11
- requires-python = ">=3.9"
12
- license = {file = "LICENSE.txt"}
11
+ requires-python = ">=3.10"
12
+ license = {text = "Apache-2.0"}
13
13
  authors = [{name = "Aric Coady", email = "aric.coady@gmail.com"}]
14
14
  keywords = ["graphql", "arrow", "parquet"]
15
15
  classifiers = [
16
16
  "Development Status :: 5 - Production/Stable",
17
17
  "Intended Audience :: Developers",
18
18
  "Intended Audience :: Science/Research",
19
- "License :: OSI Approved :: Apache Software License",
20
19
  "Operating System :: OS Independent",
21
20
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.9",
23
21
  "Programming Language :: Python :: 3.10",
24
22
  "Programming Language :: Python :: 3.11",
25
23
  "Programming Language :: Python :: 3.12",
@@ -1,4 +1,4 @@
1
- pyarrow>=18
1
+ pyarrow>=20
2
2
  strawberry-graphql[asgi,cli]>=0.236
3
3
  numpy
4
4
  isodate>=0.7
@@ -187,3 +187,5 @@ def test_not_implemented():
187
187
  pa.table({'': list('aba')}).group_by([]).aggregate([('', 'first'), ('', 'last')])
188
188
  with pytest.raises(ValueError):
189
189
  pc.pairwise_diff(pa.chunked_array([[0]]))
190
+ with pytest.raises(NotImplementedError):
191
+ pc.rank(pa.table({'': array}), [('', 'ascending')])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes