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.
Files changed (82) hide show
  1. ibis_typing-1.0.0/LICENSE +21 -0
  2. ibis_typing-1.0.0/PKG-INFO +238 -0
  3. ibis_typing-1.0.0/README.md +200 -0
  4. ibis_typing-1.0.0/ibis_typing/__init__.py +30 -0
  5. ibis_typing-1.0.0/ibis_typing/checksum_buckets.py +191 -0
  6. ibis_typing-1.0.0/ibis_typing/custom/__init__.py +0 -0
  7. ibis_typing-1.0.0/ibis_typing/custom/custom_compilers.py +155 -0
  8. ibis_typing-1.0.0/ibis_typing/custom/custom_operations.py +151 -0
  9. ibis_typing-1.0.0/ibis_typing/custom/op_cast.py +47 -0
  10. ibis_typing-1.0.0/ibis_typing/evaluator.py +223 -0
  11. ibis_typing-1.0.0/ibis_typing/expression.py +152 -0
  12. ibis_typing-1.0.0/ibis_typing/extension_method.py +96 -0
  13. ibis_typing-1.0.0/ibis_typing/fixtures/__init__.py +0 -0
  14. ibis_typing-1.0.0/ibis_typing/fixtures/duck_connection.py +19 -0
  15. ibis_typing-1.0.0/ibis_typing/fixtures/expressions.py +92 -0
  16. ibis_typing-1.0.0/ibis_typing/fixtures/fixture_marker.py +21 -0
  17. ibis_typing-1.0.0/ibis_typing/fixtures/ibis_connection.py +87 -0
  18. ibis_typing-1.0.0/ibis_typing/fixtures/ibis_time.py +33 -0
  19. ibis_typing-1.0.0/ibis_typing/fixtures/marks.py +36 -0
  20. ibis_typing-1.0.0/ibis_typing/fixtures/patch_target.py +61 -0
  21. ibis_typing-1.0.0/ibis_typing/fixtures/plugin.py +32 -0
  22. ibis_typing-1.0.0/ibis_typing/fixtures/trino_connection.py +76 -0
  23. ibis_typing-1.0.0/ibis_typing/hypothesis/__init__.py +3 -0
  24. ibis_typing-1.0.0/ibis_typing/hypothesis/strategies.py +93 -0
  25. ibis_typing-1.0.0/ibis_typing/hypothesis/tests/__init__.py +0 -0
  26. ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_joins.py +242 -0
  27. ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_one_to_many_joins.py +106 -0
  28. ibis_typing-1.0.0/ibis_typing/hypothesis/tests/test_hypothesis_transforms.py +105 -0
  29. ibis_typing-1.0.0/ibis_typing/ibis_adapter.py +109 -0
  30. ibis_typing-1.0.0/ibis_typing/ibis_connection.py +46 -0
  31. ibis_typing-1.0.0/ibis_typing/ibis_defaults.py +88 -0
  32. ibis_typing-1.0.0/ibis_typing/ibis_extension_method.py +129 -0
  33. ibis_typing-1.0.0/ibis_typing/ibis_joins.py +233 -0
  34. ibis_typing-1.0.0/ibis_typing/ibis_ops.py +87 -0
  35. ibis_typing-1.0.0/ibis_typing/ibis_pyarrow.py +246 -0
  36. ibis_typing-1.0.0/ibis_typing/ibis_time.py +145 -0
  37. ibis_typing-1.0.0/ibis_typing/ibis_types.py +183 -0
  38. ibis_typing-1.0.0/ibis_typing/ibis_utils.py +205 -0
  39. ibis_typing-1.0.0/ibis_typing/ide/__init__.py +0 -0
  40. ibis_typing-1.0.0/ibis_typing/ide/setup_ide.py +82 -0
  41. ibis_typing-1.0.0/ibis_typing/it.py +117 -0
  42. ibis_typing-1.0.0/ibis_typing/naming.py +61 -0
  43. ibis_typing-1.0.0/ibis_typing/partitioning.py +85 -0
  44. ibis_typing-1.0.0/ibis_typing/plot/__init__.py +0 -0
  45. ibis_typing-1.0.0/ibis_typing/plot/__main__.py +66 -0
  46. ibis_typing-1.0.0/ibis_typing/plot/columns/__init__.py +0 -0
  47. ibis_typing-1.0.0/ibis_typing/plot/columns/__main__.py +70 -0
  48. ibis_typing-1.0.0/ibis_typing/plot/graph.py +288 -0
  49. ibis_typing-1.0.0/ibis_typing/py.typed +0 -0
  50. ibis_typing-1.0.0/ibis_typing/reference/__init__.py +0 -0
  51. ibis_typing-1.0.0/ibis_typing/reference/py_join.py +178 -0
  52. ibis_typing-1.0.0/ibis_typing/revertible.py +82 -0
  53. ibis_typing-1.0.0/ibis_typing/samples/__init__.py +0 -0
  54. ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/__init__.py +4 -0
  55. ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/calendar_checksum_bucket.py +18 -0
  56. ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/calendar_width.py +18 -0
  57. ibis_typing-1.0.0/ibis_typing/samples/generated/sample_schemas/circle.py +20 -0
  58. ibis_typing-1.0.0/ibis_typing/samples/sample_incremental_calendar.py +45 -0
  59. ibis_typing-1.0.0/ibis_typing/samples/sample_transforms.py +27 -0
  60. ibis_typing-1.0.0/ibis_typing/samples/tests/__init__.py +0 -0
  61. ibis_typing-1.0.0/ibis_typing/samples/tests/test_circle.py +31 -0
  62. ibis_typing-1.0.0/ibis_typing/samples/tests/test_generate_ibis_schema_packages.py +24 -0
  63. ibis_typing-1.0.0/ibis_typing/schema_bindings.py +165 -0
  64. ibis_typing-1.0.0/ibis_typing/schema_writer.py +143 -0
  65. ibis_typing-1.0.0/ibis_typing/table_provider.py +111 -0
  66. ibis_typing-1.0.0/ibis_typing/table_store.py +226 -0
  67. ibis_typing-1.0.0/ibis_typing/type_patch/__init__.py +0 -0
  68. ibis_typing-1.0.0/ibis_typing/type_patch/__main__.py +15 -0
  69. ibis_typing-1.0.0/ibis_typing/type_patch/api.py +107 -0
  70. ibis_typing-1.0.0/ibis_typing/type_patch/arrays.py +89 -0
  71. ibis_typing-1.0.0/ibis_typing/type_patch/generic.py +238 -0
  72. ibis_typing-1.0.0/ibis_typing/type_patch/inspect_types.py +33 -0
  73. ibis_typing-1.0.0/ibis_typing/type_patch/json_.py +55 -0
  74. ibis_typing-1.0.0/ibis_typing/type_patch/logical.py +29 -0
  75. ibis_typing-1.0.0/ibis_typing/type_patch/maps.py +63 -0
  76. ibis_typing-1.0.0/ibis_typing/type_patch/monkeypatch.py +53 -0
  77. ibis_typing-1.0.0/ibis_typing/type_patch/numeric.py +35 -0
  78. ibis_typing-1.0.0/ibis_typing/type_patch/patched_modules.py +65 -0
  79. ibis_typing-1.0.0/ibis_typing/type_patch/patchers.py +307 -0
  80. ibis_typing-1.0.0/ibis_typing/type_patch/relations.py +380 -0
  81. ibis_typing-1.0.0/ibis_typing/utils.py +243 -0
  82. 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
+ [![PyPI](https://img.shields.io/pypi/v/ibis-typing)](https://pypi.org/project/ibis-typing/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
+ [![Python](https://img.shields.io/pypi/pyversions/ibis-typing)](https://pypi.org/project/ibis-typing/)
44
+ [![CI](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml/badge.svg)](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml)
45
+ [![Coverage](https://codecov.io/gh/FortnoxAB/ibis-typing/branch/main/graph/badge.svg)](https://codecov.io/gh/FortnoxAB/ibis-typing)
46
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
47
+ [![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)
48
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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
+ [![PyPI](https://img.shields.io/pypi/v/ibis-typing)](https://pypi.org/project/ibis-typing/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Python](https://img.shields.io/pypi/pyversions/ibis-typing)](https://pypi.org/project/ibis-typing/)
6
+ [![CI](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml/badge.svg)](https://github.com/FortnoxAB/ibis-typing/actions/workflows/ci.yml)
7
+ [![Coverage](https://codecov.io/gh/FortnoxAB/ibis-typing/branch/main/graph/badge.svg)](https://codecov.io/gh/FortnoxAB/ibis-typing)
8
+ [![Ruff](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
+ [![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
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](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