graphique 2.0.1__tar.gz → 2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {graphique-2.0.1 → graphique-2.1}/PKG-INFO +31 -21
  2. {graphique-2.0.1 → graphique-2.1}/README.md +24 -14
  3. graphique-2.1/graphique/__init__.py +2 -0
  4. graphique-2.1/graphique/core.py +100 -0
  5. {graphique-2.0.1 → graphique-2.1}/graphique/inputs.py +185 -80
  6. {graphique-2.0.1 → graphique-2.1}/graphique/interface.py +264 -103
  7. graphique-2.1/graphique/middleware.py +167 -0
  8. {graphique-2.0.1 → graphique-2.1}/graphique/models.py +49 -54
  9. graphique-2.0.1/graphique/shell.py → graphique-2.1/graphique/partition.py +16 -18
  10. graphique-2.1/graphique/scalars.py +111 -0
  11. graphique-2.1/graphique/service.py +35 -0
  12. {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/PKG-INFO +31 -21
  13. {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/SOURCES.txt +1 -1
  14. {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/requires.txt +3 -2
  15. {graphique-2.0.1 → graphique-2.1}/pyproject.toml +17 -23
  16. graphique-2.1/tests/test_core.py +76 -0
  17. graphique-2.1/tests/test_dataset.py +218 -0
  18. graphique-2.1/tests/test_models.py +206 -0
  19. graphique-2.1/tests/test_service.py +396 -0
  20. graphique-2.0.1/graphique/__init__.py +0 -3
  21. graphique-2.0.1/graphique/core.py +0 -83
  22. graphique-2.0.1/graphique/middleware.py +0 -122
  23. graphique-2.0.1/graphique/scalars.py +0 -87
  24. graphique-2.0.1/graphique/service.py +0 -44
  25. graphique-2.0.1/tests/test_core.py +0 -44
  26. graphique-2.0.1/tests/test_dataset.py +0 -167
  27. graphique-2.0.1/tests/test_models.py +0 -186
  28. graphique-2.0.1/tests/test_service.py +0 -342
  29. {graphique-2.0.1 → graphique-2.1}/LICENSE.txt +0 -0
  30. {graphique-2.0.1 → graphique-2.1}/graphique/py.typed +0 -0
  31. {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/dependency_links.txt +0 -0
  32. {graphique-2.0.1 → graphique-2.1}/graphique.egg-info/top_level.txt +0 -0
  33. {graphique-2.0.1 → graphique-2.1}/setup.cfg +0 -0
@@ -1,20 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphique
3
- Version: 2.0.1
3
+ Version: 2.1
4
4
  Summary: GraphQL service for python dataframes and parquet datasets.
5
- Author-email: Aric Coady <aric.coady@gmail.com>
5
+ Author: A. Coady
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/coady/graphique
8
8
  Project-URL: Documentation, https://coady.github.io/graphique
9
9
  Project-URL: Changelog, https://github.com/coady/graphique/blob/main/CHANGELOG.md
10
10
  Project-URL: Issues, https://github.com/coady/graphique/issues
11
- Keywords: graphql,arrow,parquet
11
+ Keywords: graphql,ibis,dataframes,arrow,parquet
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
@@ -24,11 +23,11 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
24
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
25
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
25
  Classifier: Typing :: Typed
27
- Requires-Python: >=3.10
26
+ Requires-Python: >=3.11
28
27
  Description-Content-Type: text/markdown
29
28
  License-File: LICENSE.txt
30
- Requires-Dist: pyarrow>=20
31
- Requires-Dist: strawberry-graphql[asgi,cli]>=0.285
29
+ Requires-Dist: pyarrow>=23
30
+ Requires-Dist: strawberry-graphql[asgi,cli]>=0.288
32
31
  Requires-Dist: ibis-framework>=10.7
33
32
  Requires-Dist: pyarrow-hotfix
34
33
  Requires-Dist: isodate>=0.7
@@ -37,6 +36,7 @@ Requires-Dist: uvicorn[standard]; extra == "server"
37
36
  Requires-Dist: duckdb; extra == "server"
38
37
  Provides-Extra: cli
39
38
  Requires-Dist: tqdm; extra == "cli"
39
+ Requires-Dist: typer; extra == "cli"
40
40
  Dynamic: license-file
41
41
 
42
42
  [![image](https://img.shields.io/pypi/v/graphique.svg)](https://pypi.org/project/graphique/)
@@ -47,7 +47,7 @@ Dynamic: license-file
47
47
  [![image](https://codecov.io/gh/coady/graphique/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/graphique/)
48
48
  [![CodeQL](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql)
49
49
  [![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)
50
- [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
50
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
51
51
 
52
52
  [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
53
53
 
@@ -75,25 +75,27 @@ outputs the graphql schema.
75
75
  The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
76
76
 
77
77
  * PARQUET_PATH: path to the parquet directory or file
78
- * FEDERATED = '': field name to extend type `Query` with a federated `Table`
79
- * METRICS = False: include timings from apollo tracing extension
78
+ * NAME = '': GraphQL field on `Query`; defaults to root type
80
79
  * COLUMNS = None: list of names, or mapping of aliases, of columns to select
81
- * FILTERS = None: json `filter` query for which rows to read at startup
82
80
 
83
81
  Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
84
82
 
85
83
  ### App
86
84
  For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
87
- Supply a mapping of names to datasets for multiple roots, and to enable federation.
85
+ Use a `Query` type with dataset attributes for multiple roots, and to enable federation.
88
86
 
89
87
  ```python
90
88
  import ibis
91
- from graphique import GraphQL
89
+ from graphique import GraphQL, typed
92
90
 
93
- source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
91
+ source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
94
92
  # apply initial projections or filters to `source`
95
93
  app = GraphQL(source) # Table is root query type
96
- app = GraphQL.federated({<name>: source, ...}, keys={<name>: [], ...}) # Tables on federated fields
94
+
95
+ # multiple named fields, with optional federation keys
96
+ class Query:
97
+ name = source # or `typed(source, name, keys=...)`
98
+ app = GraphQL(Query)
97
99
  ```
98
100
 
99
101
  Start like any ASGI app.
@@ -112,7 +114,8 @@ uvicorn <module>:app
112
114
  #### selection
113
115
  * `slice`: contiguous selection of rows
114
116
  * `filter`: select rows by predicates
115
- * `join`: join tables by key columns
117
+ * `join`, `asofJoin`, `crossJoin`: join tables by key columns
118
+ * `difference`, `intersect`, `union`: set operations on tables
116
119
  * `take`: rows by index
117
120
  * `dropNull`: remove rows with nulls
118
121
 
@@ -122,6 +125,7 @@ uvicorn <module>:app
122
125
  * `column`: access a column of any type by name
123
126
  * `row`: provides a field for each scalar of a single row
124
127
  * `cast`: cast column types
128
+ * `unpack`: project struct fields
125
129
  * `fillNull`: fill null values
126
130
 
127
131
  #### aggregation
@@ -129,20 +133,26 @@ uvicorn <module>:app
129
133
  * `distinct`: group with all columns
130
134
  * `runs`: provisionally group by adjacency
131
135
  * `unnest`: unnest an array column
132
- * `count`: number of rows
136
+ * `count`, `any`: number of rows
133
137
 
134
138
  #### ordering
135
139
  * `order`: sort table by given columns
136
- * options `limit` and `dense`: select rows with smallest or largest values
140
+ * `first`: provisionally sort and filter by rank
141
+
142
+ #### reflection
143
+ * `type`: type of data source
144
+ * `schema`: field names and types
145
+ * `optional`: nullable for errors
146
+ * `toSql`: compiles SQL query
137
147
 
138
148
  ### Performance
139
- Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
149
+ Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
140
150
 
141
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations, and for any feature which ibis does not support. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
151
+ [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
142
152
 
143
153
  ## Installation
144
154
  ```console
145
- pip install graphique[server]
155
+ pip install graphique[server,cli]
146
156
  ```
147
157
 
148
158
  ## Dependencies
@@ -6,7 +6,7 @@
6
6
  [![image](https://codecov.io/gh/coady/graphique/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/graphique/)
7
7
  [![CodeQL](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/coady/graphique/actions/workflows/github-code-scanning/codeql)
8
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)
9
- [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
9
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
10
10
 
11
11
  [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
12
12
 
@@ -34,25 +34,27 @@ outputs the graphql schema.
34
34
  The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
35
35
 
36
36
  * PARQUET_PATH: path to the parquet directory or file
37
- * FEDERATED = '': field name to extend type `Query` with a federated `Table`
38
- * METRICS = False: include timings from apollo tracing extension
37
+ * NAME = '': GraphQL field on `Query`; defaults to root type
39
38
  * COLUMNS = None: list of names, or mapping of aliases, of columns to select
40
- * FILTERS = None: json `filter` query for which rows to read at startup
41
39
 
42
40
  Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
43
41
 
44
42
  ### App
45
43
  For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
46
- Supply a mapping of names to datasets for multiple roots, and to enable federation.
44
+ Use a `Query` type with dataset attributes for multiple roots, and to enable federation.
47
45
 
48
46
  ```python
49
47
  import ibis
50
- from graphique import GraphQL
48
+ from graphique import GraphQL, typed
51
49
 
52
- source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
50
+ source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
53
51
  # apply initial projections or filters to `source`
54
52
  app = GraphQL(source) # Table is root query type
55
- app = GraphQL.federated({<name>: source, ...}, keys={<name>: [], ...}) # Tables on federated fields
53
+
54
+ # multiple named fields, with optional federation keys
55
+ class Query:
56
+ name = source # or `typed(source, name, keys=...)`
57
+ app = GraphQL(Query)
56
58
  ```
57
59
 
58
60
  Start like any ASGI app.
@@ -71,7 +73,8 @@ uvicorn <module>:app
71
73
  #### selection
72
74
  * `slice`: contiguous selection of rows
73
75
  * `filter`: select rows by predicates
74
- * `join`: join tables by key columns
76
+ * `join`, `asofJoin`, `crossJoin`: join tables by key columns
77
+ * `difference`, `intersect`, `union`: set operations on tables
75
78
  * `take`: rows by index
76
79
  * `dropNull`: remove rows with nulls
77
80
 
@@ -81,6 +84,7 @@ uvicorn <module>:app
81
84
  * `column`: access a column of any type by name
82
85
  * `row`: provides a field for each scalar of a single row
83
86
  * `cast`: cast column types
87
+ * `unpack`: project struct fields
84
88
  * `fillNull`: fill null values
85
89
 
86
90
  #### aggregation
@@ -88,20 +92,26 @@ uvicorn <module>:app
88
92
  * `distinct`: group with all columns
89
93
  * `runs`: provisionally group by adjacency
90
94
  * `unnest`: unnest an array column
91
- * `count`: number of rows
95
+ * `count`, `any`: number of rows
92
96
 
93
97
  #### ordering
94
98
  * `order`: sort table by given columns
95
- * options `limit` and `dense`: select rows with smallest or largest values
99
+ * `first`: provisionally sort and filter by rank
100
+
101
+ #### reflection
102
+ * `type`: type of data source
103
+ * `schema`: field names and types
104
+ * `optional`: nullable for errors
105
+ * `toSql`: compiles SQL query
96
106
 
97
107
  ### Performance
98
- Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
108
+ Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
99
109
 
100
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations, and for any feature which ibis does not support. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
110
+ [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
101
111
 
102
112
  ## Installation
103
113
  ```console
104
- pip install graphique[server]
114
+ pip install graphique[server,cli]
105
115
  ```
106
116
 
107
117
  ## Dependencies
@@ -0,0 +1,2 @@
1
+ from .interface import Dataset
2
+ from .middleware import GraphQL, MetricsExtension, implement, typed
@@ -0,0 +1,100 @@
1
+ """
2
+ Core arrow utilities.
3
+ """
4
+
5
+ import bisect
6
+ import itertools
7
+
8
+ import ibis
9
+ import pyarrow as pa
10
+ import pyarrow.dataset as ds
11
+
12
+
13
+ class links:
14
+ ref = "https://ibis-project.org/reference"
15
+ types = f"[data type]({ref}/datatypes)"
16
+ schema = f"[table schema]({ref}/schemas#ibis.expr.schema.Schema)"
17
+
18
+
19
+ def getitems(obj, *keys):
20
+ """Nested `getitem`."""
21
+ for key in keys:
22
+ obj = obj[key]
23
+ return obj
24
+
25
+
26
+ def order_key(name: str):
27
+ """Parse sort order."""
28
+ return (ibis.desc if name.startswith("-") else ibis.asc)(ibis._[name.lstrip("-")])
29
+
30
+
31
+ def rank_over(
32
+ table: ibis.Table,
33
+ by: list[str],
34
+ over: list[str],
35
+ index: ibis.Column,
36
+ rank: int = 1,
37
+ ) -> ibis.Table:
38
+ """Filter rows by rank within each grouping window."""
39
+ order_by = list(map(order_key, by))
40
+ table = table.mutate(_=index.over(group_by=over, order_by=order_by))
41
+ return table.filter(table["_"] < rank).drop("_").order_by(*order_by)
42
+
43
+
44
+ class Parquet(ds.Dataset):
45
+ """Partitioned parquet dataset."""
46
+
47
+ def schema(self) -> pa.Schema:
48
+ """partition schema"""
49
+ return self.partitioning.schema if hasattr(self, "partitioning") else pa.schema([])
50
+
51
+ def keys(self, *names) -> list:
52
+ """Return prefix of matching partition keys."""
53
+ keys = set(Parquet.schema(self).names)
54
+ return list(itertools.takewhile(lambda name: name.lstrip("-") in keys, names))
55
+
56
+ def fragments(self, counts: str = "", path: str = "__path__") -> ibis.Table:
57
+ """Return partition fragments as a table."""
58
+ parts = []
59
+ for frag in self.get_fragments():
60
+ parts.append(ds.get_partition_keys(frag.partition_expression))
61
+ parts[-1][path] = frag.path
62
+ if counts:
63
+ parts[-1][counts] = frag.count_rows()
64
+ return ibis.memtable(pa.Table.from_pylist(parts))
65
+
66
+ def filter(self, expr: ds.Expression | None) -> ds.Dataset | None:
67
+ """Attempt to apply filter to partition keys."""
68
+ try: # raises ValueError if filter references non-partition keys
69
+ ds.dataset([], schema=self.partitioning.schema).scanner(filter=expr)
70
+ except (AttributeError, ValueError):
71
+ return None
72
+ if expr is None:
73
+ return self
74
+ paths = [frag.path for frag in self.get_fragments(expr)]
75
+ return ds.dataset(paths, partitioning=self.partitioning)
76
+
77
+ def to_table(self, name: str | None = None) -> ibis.Table:
78
+ """Return ibis `Table` from filtered dataset."""
79
+ paths = [frag.path for frag in self.get_fragments()]
80
+ return ibis.read_parquet(paths, table_name=name)
81
+
82
+ def order(self, *names: str, limit: int | None = None) -> ds.Dataset:
83
+ """Return ordered partitions of the dataset."""
84
+ table = Parquet.fragments(self, counts="" if limit is None else "_")
85
+ table = table.order_by(*map(order_key, names)).cache()
86
+ if limit is not None:
87
+ limit = bisect.bisect_left(table["_"].cumsum().to_list(), limit) + 1
88
+ return ds.dataset(table[:limit]["__path__"].to_list(), partitioning=self.partitioning)
89
+
90
+ def first(self, *names: str, rank: int = 1, dense: bool = False) -> ds.Dataset:
91
+ """Return ordered partitions up to max rank (dense or sparse)."""
92
+ keys = {name.strip("-"): order_key(name) for name in names}
93
+ if dense or rank == 1:
94
+ table = Parquet.fragments(self).aggregate(_=ibis._["__path__"].collect(), by=list(keys))
95
+ paths = table.order_by(*keys.values())[:rank]["_"].unnest()
96
+ else:
97
+ table = Parquet.fragments(self, counts="_").order_by(*keys.values()).cache()
98
+ limit = bisect.bisect_left(table["_"].cumsum().to_list(), rank) + 1
99
+ paths = table.semi_join(table[:limit].select(*keys).distinct(), list(keys))["__path__"]
100
+ return ds.dataset(paths.to_list(), partitioning=self.partitioning)