ibis-typing 1.0.0__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.
- ibis_typing-1.0.0/LICENSE +21 -0
- ibis_typing-1.0.0/PKG-INFO +238 -0
- ibis_typing-1.0.0/README.md +200 -0
- ibis_typing-1.0.0/ibis_typing/__init__.py +30 -0
- ibis_typing-1.0.0/ibis_typing/checksum_buckets.py +191 -0
- ibis_typing-1.0.0/ibis_typing/custom/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/custom/custom_compilers.py +155 -0
- ibis_typing-1.0.0/ibis_typing/custom/custom_operations.py +151 -0
- ibis_typing-1.0.0/ibis_typing/custom/op_cast.py +47 -0
- ibis_typing-1.0.0/ibis_typing/evaluator.py +223 -0
- ibis_typing-1.0.0/ibis_typing/expression.py +152 -0
- ibis_typing-1.0.0/ibis_typing/extension_method.py +96 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/duck_connection.py +19 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/expressions.py +92 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/fixture_marker.py +21 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/ibis_connection.py +87 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/ibis_time.py +33 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/marks.py +36 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/patch_target.py +61 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/plugin.py +32 -0
- ibis_typing-1.0.0/ibis_typing/fixtures/trino_connection.py +76 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/__init__.py +3 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/strategies.py +93 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/tests/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_joins.py +242 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_one_to_many_joins.py +106 -0
- ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_transforms.py +105 -0
- ibis_typing-1.0.0/ibis_typing/ibis_adapter.py +109 -0
- ibis_typing-1.0.0/ibis_typing/ibis_connection.py +46 -0
- ibis_typing-1.0.0/ibis_typing/ibis_defaults.py +88 -0
- ibis_typing-1.0.0/ibis_typing/ibis_extension_method.py +129 -0
- ibis_typing-1.0.0/ibis_typing/ibis_joins.py +233 -0
- ibis_typing-1.0.0/ibis_typing/ibis_ops.py +87 -0
- ibis_typing-1.0.0/ibis_typing/ibis_pyarrow.py +246 -0
- ibis_typing-1.0.0/ibis_typing/ibis_time.py +145 -0
- ibis_typing-1.0.0/ibis_typing/ibis_types.py +183 -0
- ibis_typing-1.0.0/ibis_typing/ibis_utils.py +205 -0
- ibis_typing-1.0.0/ibis_typing/ide/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/ide/setup_ide.py +82 -0
- ibis_typing-1.0.0/ibis_typing/it.py +117 -0
- ibis_typing-1.0.0/ibis_typing/naming.py +61 -0
- ibis_typing-1.0.0/ibis_typing/partitioning.py +85 -0
- ibis_typing-1.0.0/ibis_typing/plot/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/plot/__main__.py +66 -0
- ibis_typing-1.0.0/ibis_typing/plot/columns/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/plot/columns/__main__.py +70 -0
- ibis_typing-1.0.0/ibis_typing/plot/graph.py +288 -0
- ibis_typing-1.0.0/ibis_typing/py.typed +0 -0
- ibis_typing-1.0.0/ibis_typing/reference/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/reference/py_join.py +178 -0
- ibis_typing-1.0.0/ibis_typing/revertible.py +82 -0
- ibis_typing-1.0.0/ibis_typing/samples/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/__init__.py +4 -0
- ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/calendar_checksum_bucket.py +18 -0
- ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/calendar_width.py +18 -0
- ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/circle.py +20 -0
- ibis_typing-1.0.0/ibis_typing/samples/sample_incremental_calendar.py +45 -0
- ibis_typing-1.0.0/ibis_typing/samples/sample_transforms.py +27 -0
- ibis_typing-1.0.0/ibis_typing/samples/tests/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/samples/tests/test_circle.py +31 -0
- ibis_typing-1.0.0/ibis_typing/samples/tests/test_generate_ibis_schema_packages.py +24 -0
- ibis_typing-1.0.0/ibis_typing/schema_bindings.py +165 -0
- ibis_typing-1.0.0/ibis_typing/schema_writer.py +143 -0
- ibis_typing-1.0.0/ibis_typing/table_provider.py +111 -0
- ibis_typing-1.0.0/ibis_typing/table_store.py +226 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/__init__.py +0 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/__main__.py +15 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/api.py +107 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/arrays.py +89 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/generic.py +238 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/inspect_types.py +33 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/json_.py +55 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/logical.py +29 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/maps.py +63 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/monkeypatch.py +53 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/numeric.py +35 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/patched_modules.py +65 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/patchers.py +307 -0
- ibis_typing-1.0.0/ibis_typing/type_patch/relations.py +380 -0
- ibis_typing-1.0.0/ibis_typing/utils.py +243 -0
- ibis_typing-1.0.0/pyproject.toml +191 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ibis-typing contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ibis-typing
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A typed framework for writing Ibis expressions with full IDE and static-analysis support
|
|
5
|
+
Keywords: ibis,typing,dataframe,duckdb,trino,type hints
|
|
6
|
+
Author: ibis-typing contributors
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: ibis-framework[duckdb,trino]>=9
|
|
18
|
+
Requires-Dist: sqlglot>=25
|
|
19
|
+
Requires-Dist: pyarrow>=17
|
|
20
|
+
Requires-Dist: attrs>=24
|
|
21
|
+
Requires-Dist: cattrs>=24
|
|
22
|
+
Requires-Dist: more-itertools>=10
|
|
23
|
+
Requires-Dist: typing-extensions>=4
|
|
24
|
+
Requires-Dist: hypothesis>=6
|
|
25
|
+
Requires-Dist: networkx>=3
|
|
26
|
+
Requires-Dist: matplotlib>=3
|
|
27
|
+
Requires-Dist: pydot>=4
|
|
28
|
+
Requires-Dist: graphviz>=0.21
|
|
29
|
+
Requires-Dist: humanize>=4
|
|
30
|
+
Requires-Dist: pytest>=8 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: testcontainers>=4 ; extra == 'dev'
|
|
32
|
+
Requires-Python: >=3.12
|
|
33
|
+
Project-URL: Homepage, https://github.com/FortnoxAB/ibis-typing
|
|
34
|
+
Project-URL: Repository, https://github.com/FortnoxAB/ibis-typing
|
|
35
|
+
Project-URL: Issues, https://github.com/FortnoxAB/ibis-typing/issues
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# ibis-typing
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/ibis-typing/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://pypi.org/project/ibis-typing/)
|
|
44
|
+
[](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml)
|
|
45
|
+
[](https://codecov.io/gh/FortnoxAB/ibis-typing)
|
|
46
|
+
[](https://github.com/astral-sh/ruff)
|
|
47
|
+
[](https://github.com/astral-sh/ty)
|
|
48
|
+
[](https://github.com/astral-sh/uv)
|
|
49
|
+
|
|
50
|
+
A typed framework for writing [Ibis](https://ibis-project.org/) dataframe expressions — with full IDE support, static analysis, and property-based testing.
|
|
51
|
+
|
|
52
|
+
[Ibis](https://ibis-project.org/) is a portable Python dataframe library (DSL) that runs on DuckDB, Polars, Trino, BigQuery, and more. **ibis-typing** layers a type-safe schema system on top of it, so your transforms carry type information end-to-end.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install ibis-typing
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv add ibis-typing
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After installation, run the type-patch step once to inject typed overloads into your installed `ibis` package:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m ibis_typing.type_patch
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
### 1. Define schemas
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from attrs import frozen
|
|
76
|
+
from ibis_typing import IbisSchema, it
|
|
77
|
+
|
|
78
|
+
@frozen
|
|
79
|
+
class Transaction(IbisSchema):
|
|
80
|
+
date: it.Date = None
|
|
81
|
+
amount: it.Float64 = None
|
|
82
|
+
category: it.String = None
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Define a typed expression
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from ibis_typing import Expression, IbisTable, this, it
|
|
89
|
+
|
|
90
|
+
@frozen
|
|
91
|
+
class MonthlyAmounts(Expression):
|
|
92
|
+
month: it.Date = None
|
|
93
|
+
amount: it.Float64 = None
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_expression(cls, inputs: IbisTable[Transaction]):
|
|
97
|
+
cols = inputs.cols
|
|
98
|
+
table = (
|
|
99
|
+
inputs.table
|
|
100
|
+
@ it.Select(expr={"month": this[cols.date].truncate("M")})
|
|
101
|
+
@ it.Aggregate(by=["month"], sum=[cols.amount])
|
|
102
|
+
)
|
|
103
|
+
return cls.of(table)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> **Tip:** `IbisSchema` classes for your `Expression` outputs can be generated automatically using `ibis_typing.schema_writer`. The code is backend-agnostic — schemas are derived from abstract Ibis table schemas, so no live backend is required.
|
|
107
|
+
|
|
108
|
+
### 3. Evaluate against a backend
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from datetime import date
|
|
112
|
+
|
|
113
|
+
from ibis_typing import IbisConnection, evaluator
|
|
114
|
+
|
|
115
|
+
conn = IbisConnection()
|
|
116
|
+
transactions = Transaction.of_rows([Transaction(date=date(2024, 1, 15), amount=100.0, category="A")])
|
|
117
|
+
monthly_amounts = evaluator.from_expression(MonthlyAmounts, transactions)
|
|
118
|
+
results: list[MonthlyAmounts] = list(conn.fetch_table(monthly_amounts))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Test with Hypothesis
|
|
122
|
+
|
|
123
|
+
pytest fixtures are registered automatically — no `conftest.py` needed.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from hypothesis import given, strategies as st
|
|
127
|
+
from ibis_typing.hypothesis import strategy_for
|
|
128
|
+
|
|
129
|
+
@given(st.lists(strategy_for(Transaction), min_size=1))
|
|
130
|
+
def test_monthly_amounts(evaluate_table, transactions):
|
|
131
|
+
actual, expected = evaluate_table(MonthlyAmounts, transactions)
|
|
132
|
+
assert sorted(actual) == sorted(expected)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Core concepts
|
|
136
|
+
|
|
137
|
+
```mermaid
|
|
138
|
+
graph TD
|
|
139
|
+
IbisSchema -->|describes| IbisTable
|
|
140
|
+
IbisTable -->|In| Expression
|
|
141
|
+
Expression -->|Out| IbisTable
|
|
142
|
+
TableProvider -->|provides tables to| Expression
|
|
143
|
+
IbisConnection -->|fetches typed rows from| IbisTable
|
|
144
|
+
ChecksumBuckets -->|incremental inputs to| IncrementalExpression
|
|
145
|
+
IncrementalExpression -->|is a| Expression
|
|
146
|
+
BucketedInputsExpression -->|is a| IncrementalExpression
|
|
147
|
+
RevertibleTableExpression -->|can revert| Expression
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Class | Purpose |
|
|
151
|
+
|-----------------------------|---|
|
|
152
|
+
| `IbisSchema` | Base class for typed table schemas (attrs frozen dataclass) |
|
|
153
|
+
| `IbisTable[S]` | Generic typed wrapper around `ibis.Table` |
|
|
154
|
+
| `Expression` | Abstract base for typed ibis transforms |
|
|
155
|
+
| `IbisConnection` | Typed backend wrapper: `fetch_table()`, `evaluate()`, `read/write_parquet()` |
|
|
156
|
+
| `BucketedInputsExpression` | Expression that only re-runs for changed input buckets |
|
|
157
|
+
| `ChecksumBuckets` | Checksum-based incremental input tracking |
|
|
158
|
+
| `RevertibleTableExpression` | Transform that can undo itself back to the original schema |
|
|
159
|
+
|
|
160
|
+
## Type aliases
|
|
161
|
+
|
|
162
|
+
Declare schema fields using column-type aliases from `ibis_typing.it`:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from ibis_typing import it
|
|
166
|
+
|
|
167
|
+
it.Int8, it.Int16, it.Int32, it.Int64
|
|
168
|
+
it.Float32, it.Float64
|
|
169
|
+
it.Boolean
|
|
170
|
+
it.String, it.Binary
|
|
171
|
+
it.Decimal
|
|
172
|
+
it.Date, it.Time, it.Timestamp
|
|
173
|
+
it.UUID, it.JSON
|
|
174
|
+
it.Array[it.Int64]
|
|
175
|
+
it.Map[it.String, it.Float64]
|
|
176
|
+
it.Struct[MyTypedDict]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Table operations
|
|
180
|
+
|
|
181
|
+
Use the infix `@` operator for composable, typed table transforms:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from ibis_typing import IbisSchema, IbisTable, this, it
|
|
185
|
+
|
|
186
|
+
@frozen
|
|
187
|
+
class InputSchema(IbisSchema):
|
|
188
|
+
a: it.Float64 = None
|
|
189
|
+
b: it.Float64 = None
|
|
190
|
+
category: it.String = None
|
|
191
|
+
amount: it.Float64 = None
|
|
192
|
+
key: it.String = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
inputs: IbisTable[InputSchema] = ...
|
|
196
|
+
other_table: IbisTable = ...
|
|
197
|
+
cols = InputSchema.cols
|
|
198
|
+
|
|
199
|
+
table = InputSchema.of(
|
|
200
|
+
inputs.table
|
|
201
|
+
@ it.Select(cols.a, cols.b, expr={"c": this[cols.a] + this[cols.b]})
|
|
202
|
+
@ it.Aggregate(by=[cols.category], sum=[cols.amount])
|
|
203
|
+
@ it.InnerJoin(other_table.table, keys=[cols.key])
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Pytest fixtures
|
|
208
|
+
|
|
209
|
+
The following fixtures are auto-registered via the pytest plugin entry point (no `conftest.py` needed):
|
|
210
|
+
|
|
211
|
+
| Fixture | Purpose |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `evaluate_table` | Runs an `Expression`, returns `(actual, expected)` row lists |
|
|
214
|
+
| `fetch_table` | Fetches rows from an `IbisTable` via DuckDB |
|
|
215
|
+
| `ibis_connection` | Provides a DuckDB-backed `IbisConnection` |
|
|
216
|
+
|
|
217
|
+
## Extras
|
|
218
|
+
|
|
219
|
+
- **`ibis_typing.type_patch`** — patches installed ibis with typed `@overload` stubs for `ibis.ifelse`, `ibis.cases`, `ibis.coalesce`, etc.
|
|
220
|
+
- **`ibis_typing.schema_writer`** — code-gen: write `IbisSchema` `.py` files from `Expression` output schemas
|
|
221
|
+
- **`ibis_typing.plot`** — plots the dependency graph of an `Expression` using matplotlib/graphviz
|
|
222
|
+
- **`ibis_typing.custom`** — custom ibis operations: `DateAddMonth`, `DateAddDay`, `ColumnChecksum`, `JsonParse`, `JsonFormat`, `UUIDFromInt`, `LuhnCheck`
|
|
223
|
+
|
|
224
|
+
## Contributing
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
git clone https://github.com/FortnoxAB/ibis-typing
|
|
228
|
+
cd ibis-typing
|
|
229
|
+
uv sync --all-extras
|
|
230
|
+
uv run python -m ibis_typing.type_patch
|
|
231
|
+
make test
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Pull requests welcome. Please run `make` before submitting.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# ibis-typing
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ibis-typing/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://pypi.org/project/ibis-typing/)
|
|
6
|
+
[](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/FortnoxAB/ibis-typing)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
[](https://github.com/astral-sh/ty)
|
|
10
|
+
[](https://github.com/astral-sh/uv)
|
|
11
|
+
|
|
12
|
+
A typed framework for writing [Ibis](https://ibis-project.org/) dataframe expressions — with full IDE support, static analysis, and property-based testing.
|
|
13
|
+
|
|
14
|
+
[Ibis](https://ibis-project.org/) is a portable Python dataframe library (DSL) that runs on DuckDB, Polars, Trino, BigQuery, and more. **ibis-typing** layers a type-safe schema system on top of it, so your transforms carry type information end-to-end.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install ibis-typing
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add ibis-typing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
After installation, run the type-patch step once to inject typed overloads into your installed `ibis` package:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m ibis_typing.type_patch
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
### 1. Define schemas
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from attrs import frozen
|
|
38
|
+
from ibis_typing import IbisSchema, it
|
|
39
|
+
|
|
40
|
+
@frozen
|
|
41
|
+
class Transaction(IbisSchema):
|
|
42
|
+
date: it.Date = None
|
|
43
|
+
amount: it.Float64 = None
|
|
44
|
+
category: it.String = None
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Define a typed expression
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from ibis_typing import Expression, IbisTable, this, it
|
|
51
|
+
|
|
52
|
+
@frozen
|
|
53
|
+
class MonthlyAmounts(Expression):
|
|
54
|
+
month: it.Date = None
|
|
55
|
+
amount: it.Float64 = None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_expression(cls, inputs: IbisTable[Transaction]):
|
|
59
|
+
cols = inputs.cols
|
|
60
|
+
table = (
|
|
61
|
+
inputs.table
|
|
62
|
+
@ it.Select(expr={"month": this[cols.date].truncate("M")})
|
|
63
|
+
@ it.Aggregate(by=["month"], sum=[cols.amount])
|
|
64
|
+
)
|
|
65
|
+
return cls.of(table)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Tip:** `IbisSchema` classes for your `Expression` outputs can be generated automatically using `ibis_typing.schema_writer`. The code is backend-agnostic — schemas are derived from abstract Ibis table schemas, so no live backend is required.
|
|
69
|
+
|
|
70
|
+
### 3. Evaluate against a backend
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from datetime import date
|
|
74
|
+
|
|
75
|
+
from ibis_typing import IbisConnection, evaluator
|
|
76
|
+
|
|
77
|
+
conn = IbisConnection()
|
|
78
|
+
transactions = Transaction.of_rows([Transaction(date=date(2024, 1, 15), amount=100.0, category="A")])
|
|
79
|
+
monthly_amounts = evaluator.from_expression(MonthlyAmounts, transactions)
|
|
80
|
+
results: list[MonthlyAmounts] = list(conn.fetch_table(monthly_amounts))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Test with Hypothesis
|
|
84
|
+
|
|
85
|
+
pytest fixtures are registered automatically — no `conftest.py` needed.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from hypothesis import given, strategies as st
|
|
89
|
+
from ibis_typing.hypothesis import strategy_for
|
|
90
|
+
|
|
91
|
+
@given(st.lists(strategy_for(Transaction), min_size=1))
|
|
92
|
+
def test_monthly_amounts(evaluate_table, transactions):
|
|
93
|
+
actual, expected = evaluate_table(MonthlyAmounts, transactions)
|
|
94
|
+
assert sorted(actual) == sorted(expected)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Core concepts
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
graph TD
|
|
101
|
+
IbisSchema -->|describes| IbisTable
|
|
102
|
+
IbisTable -->|In| Expression
|
|
103
|
+
Expression -->|Out| IbisTable
|
|
104
|
+
TableProvider -->|provides tables to| Expression
|
|
105
|
+
IbisConnection -->|fetches typed rows from| IbisTable
|
|
106
|
+
ChecksumBuckets -->|incremental inputs to| IncrementalExpression
|
|
107
|
+
IncrementalExpression -->|is a| Expression
|
|
108
|
+
BucketedInputsExpression -->|is a| IncrementalExpression
|
|
109
|
+
RevertibleTableExpression -->|can revert| Expression
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Class | Purpose |
|
|
113
|
+
|-----------------------------|---|
|
|
114
|
+
| `IbisSchema` | Base class for typed table schemas (attrs frozen dataclass) |
|
|
115
|
+
| `IbisTable[S]` | Generic typed wrapper around `ibis.Table` |
|
|
116
|
+
| `Expression` | Abstract base for typed ibis transforms |
|
|
117
|
+
| `IbisConnection` | Typed backend wrapper: `fetch_table()`, `evaluate()`, `read/write_parquet()` |
|
|
118
|
+
| `BucketedInputsExpression` | Expression that only re-runs for changed input buckets |
|
|
119
|
+
| `ChecksumBuckets` | Checksum-based incremental input tracking |
|
|
120
|
+
| `RevertibleTableExpression` | Transform that can undo itself back to the original schema |
|
|
121
|
+
|
|
122
|
+
## Type aliases
|
|
123
|
+
|
|
124
|
+
Declare schema fields using column-type aliases from `ibis_typing.it`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from ibis_typing import it
|
|
128
|
+
|
|
129
|
+
it.Int8, it.Int16, it.Int32, it.Int64
|
|
130
|
+
it.Float32, it.Float64
|
|
131
|
+
it.Boolean
|
|
132
|
+
it.String, it.Binary
|
|
133
|
+
it.Decimal
|
|
134
|
+
it.Date, it.Time, it.Timestamp
|
|
135
|
+
it.UUID, it.JSON
|
|
136
|
+
it.Array[it.Int64]
|
|
137
|
+
it.Map[it.String, it.Float64]
|
|
138
|
+
it.Struct[MyTypedDict]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Table operations
|
|
142
|
+
|
|
143
|
+
Use the infix `@` operator for composable, typed table transforms:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from ibis_typing import IbisSchema, IbisTable, this, it
|
|
147
|
+
|
|
148
|
+
@frozen
|
|
149
|
+
class InputSchema(IbisSchema):
|
|
150
|
+
a: it.Float64 = None
|
|
151
|
+
b: it.Float64 = None
|
|
152
|
+
category: it.String = None
|
|
153
|
+
amount: it.Float64 = None
|
|
154
|
+
key: it.String = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
inputs: IbisTable[InputSchema] = ...
|
|
158
|
+
other_table: IbisTable = ...
|
|
159
|
+
cols = InputSchema.cols
|
|
160
|
+
|
|
161
|
+
table = InputSchema.of(
|
|
162
|
+
inputs.table
|
|
163
|
+
@ it.Select(cols.a, cols.b, expr={"c": this[cols.a] + this[cols.b]})
|
|
164
|
+
@ it.Aggregate(by=[cols.category], sum=[cols.amount])
|
|
165
|
+
@ it.InnerJoin(other_table.table, keys=[cols.key])
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Pytest fixtures
|
|
170
|
+
|
|
171
|
+
The following fixtures are auto-registered via the pytest plugin entry point (no `conftest.py` needed):
|
|
172
|
+
|
|
173
|
+
| Fixture | Purpose |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `evaluate_table` | Runs an `Expression`, returns `(actual, expected)` row lists |
|
|
176
|
+
| `fetch_table` | Fetches rows from an `IbisTable` via DuckDB |
|
|
177
|
+
| `ibis_connection` | Provides a DuckDB-backed `IbisConnection` |
|
|
178
|
+
|
|
179
|
+
## Extras
|
|
180
|
+
|
|
181
|
+
- **`ibis_typing.type_patch`** — patches installed ibis with typed `@overload` stubs for `ibis.ifelse`, `ibis.cases`, `ibis.coalesce`, etc.
|
|
182
|
+
- **`ibis_typing.schema_writer`** — code-gen: write `IbisSchema` `.py` files from `Expression` output schemas
|
|
183
|
+
- **`ibis_typing.plot`** — plots the dependency graph of an `Expression` using matplotlib/graphviz
|
|
184
|
+
- **`ibis_typing.custom`** — custom ibis operations: `DateAddMonth`, `DateAddDay`, `ColumnChecksum`, `JsonParse`, `JsonFormat`, `UUIDFromInt`, `LuhnCheck`
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
git clone https://github.com/FortnoxAB/ibis-typing
|
|
190
|
+
cd ibis-typing
|
|
191
|
+
uv sync --all-extras
|
|
192
|
+
uv run python -m ibis_typing.type_patch
|
|
193
|
+
make test
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Pull requests welcome. Please run `make` before submitting.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Facade for collected ibis-typing imports."""
|
|
2
|
+
|
|
3
|
+
from ibis.expr import datatypes as dt
|
|
4
|
+
|
|
5
|
+
# ruff: noqa # Ensure no cyclical imports
|
|
6
|
+
from .fixtures.patch_target import PatchTarget
|
|
7
|
+
from .ibis_adapter import IbisDbSchema, IbisSchema, IbisTable, this
|
|
8
|
+
from .ibis_connection import IbisConnection
|
|
9
|
+
from .expression import Expression
|
|
10
|
+
from .revertible import RevertibleTableExpression
|
|
11
|
+
from .checksum_buckets import (
|
|
12
|
+
ChecksumBuckets,
|
|
13
|
+
BucketedInputsExpression,
|
|
14
|
+
IncrementalExpression,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BucketedInputsExpression",
|
|
19
|
+
"ChecksumBuckets",
|
|
20
|
+
"Expression",
|
|
21
|
+
"IbisConnection",
|
|
22
|
+
"IbisDbSchema",
|
|
23
|
+
"IbisSchema",
|
|
24
|
+
"IbisTable",
|
|
25
|
+
"IncrementalExpression",
|
|
26
|
+
"PatchTarget",
|
|
27
|
+
"RevertibleTableExpression",
|
|
28
|
+
"dt",
|
|
29
|
+
"this",
|
|
30
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import operator
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from attrs import frozen
|
|
9
|
+
|
|
10
|
+
from . import ibis_types as it
|
|
11
|
+
from .expression import (
|
|
12
|
+
Expression,
|
|
13
|
+
GenericExpression,
|
|
14
|
+
TableExpression,
|
|
15
|
+
)
|
|
16
|
+
from .ibis_adapter import IbisSchema, IbisTable, this
|
|
17
|
+
from .ibis_extension_method import deferred
|
|
18
|
+
from .ibis_joins import LeftJoin, OuterJoin
|
|
19
|
+
from .ibis_ops import ColumnChecksum
|
|
20
|
+
from .ibis_time import TimestampNow
|
|
21
|
+
from .ibis_utils import Aggregate
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BucketedInputsExpression",
|
|
25
|
+
"BucketedInputsParams",
|
|
26
|
+
"ChecksumBuckets",
|
|
27
|
+
"ChecksumParams",
|
|
28
|
+
"IncrementalExpression",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@frozen
|
|
33
|
+
class IncrementalParams:
|
|
34
|
+
group_by: Sequence[it.NameOrType]
|
|
35
|
+
updated_at_col: it.Timestamp = "checksum_updated_at" # type: ignore
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@frozen(kw_only=True)
|
|
39
|
+
class ChecksumParams(IncrementalParams):
|
|
40
|
+
inputs: type[IbisSchema]
|
|
41
|
+
checksum_col: it.Int64 = "checksum" # type: ignore
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@frozen(kw_only=True)
|
|
45
|
+
class BucketedInputsParams(IncrementalParams):
|
|
46
|
+
buckets: type[ChecksumBuckets]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IncrementalExpression(Expression):
|
|
50
|
+
"""Expressions that can be updated incrementally."""
|
|
51
|
+
|
|
52
|
+
incremental_params: ClassVar[IncrementalParams]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BucketedInputsExpression(IncrementalExpression):
|
|
56
|
+
"""Base class for implementing IncrementalExpressions.
|
|
57
|
+
|
|
58
|
+
On incremental updates,
|
|
59
|
+
the expression is only provided inputs from updated ChecksumBuckets.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
incremental_params: ClassVar[BucketedInputsParams] # constant
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_parameter_schema_types(cls):
|
|
66
|
+
# Replace plain IbisTable[Inputs] with IbisTable[BucketedInputs] variant.
|
|
67
|
+
params = super().get_parameter_schema_types()
|
|
68
|
+
buckets = cls.incremental_params.buckets
|
|
69
|
+
inputs = buckets.incremental_params.inputs
|
|
70
|
+
|
|
71
|
+
bucketed_inputs = BucketedInputsTableExpression(buckets).as_expression_schema()
|
|
72
|
+
|
|
73
|
+
if inputs not in params.values():
|
|
74
|
+
raise TypeError(f"{cls.__name__} lacks {ChecksumBuckets.__name__} input.")
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
name: bucketed_inputs if issubclass(schema, inputs) else schema
|
|
78
|
+
for name, schema in params.items()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@frozen
|
|
83
|
+
class ChecksumBucketsTableExpression(TableExpression):
|
|
84
|
+
params: ChecksumParams
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def input_schemas(self):
|
|
88
|
+
return {"inputs": self.params.inputs, "timestamp": TimestampNow}
|
|
89
|
+
|
|
90
|
+
def __call__(self, inputs: IbisTable, timestamp: IbisTable[TimestampNow]):
|
|
91
|
+
args = self.params
|
|
92
|
+
checksums = [
|
|
93
|
+
this[col_name] @ ColumnChecksum()
|
|
94
|
+
for col_name in sorted(inputs.table.columns)
|
|
95
|
+
if col_name not in args.group_by
|
|
96
|
+
]
|
|
97
|
+
return (
|
|
98
|
+
inputs.table
|
|
99
|
+
@ Aggregate(
|
|
100
|
+
by=args.group_by,
|
|
101
|
+
expr={args.checksum_col: functools.reduce(operator.xor, checksums)},
|
|
102
|
+
)
|
|
103
|
+
@ deferred.join(timestamp.table)
|
|
104
|
+
.rename({args.updated_at_col: timestamp.cols.timestamp})
|
|
105
|
+
.relocate(args.updated_at_col, before=args.checksum_col)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ChecksumBuckets(IncrementalExpression, GenericExpression):
|
|
110
|
+
"""Checksum for inputs grouped by specific key columns."""
|
|
111
|
+
|
|
112
|
+
incremental_params: ClassVar[ChecksumParams] # constant
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_table_expression(cls):
|
|
116
|
+
return ChecksumBucketsTableExpression(cls.incremental_params)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def construct_increment[E: ChecksumBuckets](
|
|
120
|
+
cls: type[E],
|
|
121
|
+
buckets: IbisTable[E],
|
|
122
|
+
prior: IbisTable[E],
|
|
123
|
+
timestamp: IbisTable[TimestampNow],
|
|
124
|
+
) -> IbisTable[E]:
|
|
125
|
+
"""Calculate updated ChecksumBuckets table."""
|
|
126
|
+
args = cls.incremental_params
|
|
127
|
+
|
|
128
|
+
table = (
|
|
129
|
+
buckets.table
|
|
130
|
+
@ OuterJoin(prior.table, keys=args.group_by)
|
|
131
|
+
@ deferred.fill_null({args.checksum_col: 0})
|
|
132
|
+
.filter(
|
|
133
|
+
~this[args.checksum_col].identical_to(
|
|
134
|
+
this[f"{args.checksum_col}_right"]
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
.join(timestamp.table)
|
|
138
|
+
.rename({args.updated_at_col: timestamp.cols.timestamp})
|
|
139
|
+
.relocate(args.updated_at_col, before=args.checksum_col)
|
|
140
|
+
.select(buckets.table.columns)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return cls.of(table)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@frozen
|
|
147
|
+
class ChecksumBucketsIncrementTableExpression(TableExpression):
|
|
148
|
+
buckets: type[ChecksumBuckets]
|
|
149
|
+
target: type[IncrementalExpression]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def input_schemas(self):
|
|
153
|
+
return {"buckets": self.buckets, "target": self.target}
|
|
154
|
+
|
|
155
|
+
def __call__(self, buckets: IbisTable, target: IbisTable):
|
|
156
|
+
target_updated_at = self.target.incremental_params.updated_at_col
|
|
157
|
+
bucket_updated_at = self.buckets.incremental_params.updated_at_col
|
|
158
|
+
|
|
159
|
+
is_updated = this[bucket_updated_at] > target.table[target_updated_at].max()
|
|
160
|
+
|
|
161
|
+
return buckets.table.filter(is_updated)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def generated_class_name(self) -> str:
|
|
165
|
+
return f"{self.buckets.__name__}Increment"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@frozen
|
|
169
|
+
class BucketedInputsTableExpression(TableExpression):
|
|
170
|
+
buckets: type[ChecksumBuckets]
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def input_schemas(self):
|
|
174
|
+
inputs = self.buckets.incremental_params.inputs
|
|
175
|
+
return {"buckets": self.buckets, "inputs": inputs}
|
|
176
|
+
|
|
177
|
+
def __call__(self, buckets: IbisTable, inputs: IbisTable):
|
|
178
|
+
args = self.buckets.incremental_params
|
|
179
|
+
return (
|
|
180
|
+
buckets.table
|
|
181
|
+
@ LeftJoin(
|
|
182
|
+
inputs.table,
|
|
183
|
+
keys=args.group_by,
|
|
184
|
+
)
|
|
185
|
+
@ deferred.drop(args.checksum_col)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def generated_class_name(self) -> str:
|
|
190
|
+
inputs = self.buckets.incremental_params.inputs
|
|
191
|
+
return f"{inputs.__name__}BucketedInputs"
|
|
File without changes
|