interloper-sql 0.111.0__tar.gz → 0.113.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.
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/PKG-INFO +1 -1
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/pyproject.toml +1 -1
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/pyproject.toml.orig +1 -1
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/connection.py +1 -1
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/destination.py +14 -13
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/view/base.py +29 -41
- interloper_sql-0.113.0/src/interloper_sql/view/decorator.py +29 -0
- interloper_sql-0.111.0/src/interloper_sql/view/decorator.py +0 -84
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/README.md +0 -0
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/__init__.py +0 -0
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/types.py +0 -0
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/view/__init__.py +0 -0
- {interloper_sql-0.111.0 → interloper_sql-0.113.0}/src/interloper_sql/view/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: interloper-sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.113.0
|
|
4
4
|
Summary: Interloper SQL integration: portable views over SQL warehouses, and a SQLAlchemy-backed destination for PostgreSQL and other SQL databases
|
|
5
5
|
Author: Guillaume Onfroy
|
|
6
6
|
Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "interloper-sql"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.113.0"
|
|
4
4
|
description = "Interloper SQL integration: portable views over SQL warehouses, and a SQLAlchemy-backed destination for PostgreSQL and other SQL databases"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ###############
|
|
4
4
|
[project]
|
|
5
5
|
name = "interloper-sql"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.113.0"
|
|
7
7
|
description = "Interloper SQL integration: portable views over SQL warehouses, and a SQLAlchemy-backed destination for PostgreSQL and other SQL databases"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
|
|
@@ -6,7 +6,7 @@ from functools import cached_property
|
|
|
6
6
|
|
|
7
7
|
import sqlalchemy
|
|
8
8
|
from interloper.connection import Connection, connection
|
|
9
|
-
from interloper.resource
|
|
9
|
+
from interloper.resource import SecretField, fetch_field_provider
|
|
10
10
|
from pydantic_settings import SettingsConfigDict
|
|
11
11
|
from sqlalchemy.engine import Engine
|
|
12
12
|
|
|
@@ -10,11 +10,11 @@ from collections.abc import Iterator, Sequence
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
from interloper.destination import IOContext, destination
|
|
14
|
-
from interloper.destination.database import DatabaseDestination, PartitionFilter
|
|
13
|
+
from interloper.destination import DatabaseDestination, IOContext, destination
|
|
15
14
|
from interloper.errors import DataNotFoundError
|
|
16
|
-
from interloper.
|
|
17
|
-
from interloper.
|
|
15
|
+
from interloper.partitioning import PartitionFilter
|
|
16
|
+
from interloper.representation import TableData
|
|
17
|
+
from interloper.resource import InputField
|
|
18
18
|
from interloper.schema import FieldSpec
|
|
19
19
|
from pydantic import PrivateAttr
|
|
20
20
|
from sqlalchemy import Column, MetaData, String, Table, and_, cast, func, inspect, select
|
|
@@ -186,13 +186,13 @@ class SQLDestination(DatabaseDestination):
|
|
|
186
186
|
context: IO context carrying the effective schema.
|
|
187
187
|
"""
|
|
188
188
|
dataset = self._resolve_dataset(dataset)
|
|
189
|
-
|
|
189
|
+
table_data = TableData.from_data(data)
|
|
190
190
|
with self._begin() as conn:
|
|
191
191
|
sql_table = self._table(table, dataset, conn)
|
|
192
192
|
if sql_table is None:
|
|
193
|
-
schema = context.schema if context.schema is not None else
|
|
193
|
+
schema = context.schema if context.schema is not None else table_data.infer()
|
|
194
194
|
sql_table = self._create_table(table, dataset, schema.field_specs(), conn)
|
|
195
|
-
rows = _align(
|
|
195
|
+
rows = _align(table_data.rows, [c.name for c in sql_table.columns], self._ref(table, dataset))
|
|
196
196
|
for start in range(0, len(rows), _BATCH_SIZE):
|
|
197
197
|
conn.execute(sql_table.insert(), rows[start : start + _BATCH_SIZE])
|
|
198
198
|
|
|
@@ -312,22 +312,23 @@ def _predicate(table: Table, where: PartitionFilter) -> ColumnElement[bool]:
|
|
|
312
312
|
|
|
313
313
|
Args:
|
|
314
314
|
table: The reflected table.
|
|
315
|
-
where: The filter:
|
|
315
|
+
where: The filter: membership of ``values``, or half-open ``bounds``.
|
|
316
316
|
|
|
317
317
|
Returns:
|
|
318
318
|
The ``WHERE`` clause.
|
|
319
319
|
"""
|
|
320
320
|
column = table.c[where.column]
|
|
321
|
-
if where.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
if where.values is not None:
|
|
322
|
+
return column.in_([_coerce(column, value) for value in where.values])
|
|
323
|
+
assert where.bounds is not None
|
|
324
|
+
start, end = where.bounds
|
|
325
|
+
return and_(column >= _coerce(column, start), column < _coerce(column, end))
|
|
325
326
|
|
|
326
327
|
|
|
327
328
|
def _coerce(column: Column[Any], value: Any) -> Any:
|
|
328
329
|
"""Coerce a filter value to the Python type a date or timestamp column binds.
|
|
329
330
|
|
|
330
|
-
A partition
|
|
331
|
+
A partition key arrives as a string (``"2024-01-01"``) and a day's bounds
|
|
331
332
|
as dates; bound as-is against a ``DATE`` or ``TIMESTAMP`` column, they
|
|
332
333
|
would be sent as ``VARCHAR`` or rejected by the dialect's type.
|
|
333
334
|
|
|
@@ -6,16 +6,15 @@ from collections.abc import Callable, Sequence
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
import ibis
|
|
9
|
-
from interloper.asset import Upstream, View
|
|
10
|
-
from interloper.destination
|
|
9
|
+
from interloper.asset import Output, Upstream, View
|
|
10
|
+
from interloper.destination import TableReference
|
|
11
11
|
from interloper.errors import ConfigError
|
|
12
12
|
|
|
13
13
|
from interloper_sql.view.types import IBIS_DIALECTS, ibis_schema
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from interloper.asset import Asset
|
|
17
|
-
from interloper.
|
|
18
|
-
from interloper.partitioning.base import Partition, PartitionWindow
|
|
17
|
+
from interloper.partitioning import PartitionWindow
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
class IbisDefinition:
|
|
@@ -56,56 +55,45 @@ class IbisView(View):
|
|
|
56
55
|
compiles it for its own dialect.
|
|
57
56
|
"""
|
|
58
57
|
|
|
59
|
-
async def
|
|
60
|
-
self,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
dag: DAG | None,
|
|
64
|
-
partition_or_window: Partition | PartitionWindow | None,
|
|
65
|
-
metadata: dict[str, Any],
|
|
66
|
-
) -> list[Upstream]:
|
|
67
|
-
"""Turn every table reference into a typed Ibis table.
|
|
58
|
+
async def _upstream(
|
|
59
|
+
self, name: str, target: Asset, window: PartitionWindow | None, metadata: dict[str, Any]
|
|
60
|
+
) -> Upstream:
|
|
61
|
+
"""Turn the upstream's table reference into a typed Ibis table.
|
|
68
62
|
|
|
69
63
|
Args:
|
|
70
|
-
name: The ``data()`` parameter the
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
partition_or_window: Forwarded.
|
|
64
|
+
name: The ``data()`` parameter the upstream is bound to.
|
|
65
|
+
target: The upstream asset.
|
|
66
|
+
window: Forwarded; a reference has no scope.
|
|
74
67
|
metadata: Forwarded.
|
|
75
68
|
|
|
76
69
|
Returns:
|
|
77
|
-
|
|
70
|
+
The upstream, its ``data`` an ``ibis.Table``.
|
|
78
71
|
|
|
79
72
|
Raises:
|
|
80
|
-
ConfigError: If
|
|
73
|
+
ConfigError: If the upstream declares no schema, or its reference is not a table.
|
|
81
74
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
location =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Upstream(asset=upstream.asset, data=ibis.table(ibis_schema(reference.schema), name=table, **location))
|
|
98
|
-
)
|
|
99
|
-
return tables
|
|
100
|
-
|
|
101
|
-
def _definition(self, result: Any) -> IbisDefinition:
|
|
75
|
+
upstream = await super()._upstream(name, target, window, metadata)
|
|
76
|
+
reference = upstream.data
|
|
77
|
+
if not isinstance(reference, TableReference):
|
|
78
|
+
raise ConfigError(f"Upstream '{upstream.asset.qualified_key}' is not a table: {reference!r}")
|
|
79
|
+
if reference.schema is None:
|
|
80
|
+
raise ConfigError(f"Upstream '{upstream.asset.qualified_key}' declares no schema to type its table")
|
|
81
|
+
*outer, table = reference.parts
|
|
82
|
+
location = {}
|
|
83
|
+
if outer:
|
|
84
|
+
location["database"] = outer[-1]
|
|
85
|
+
if len(outer) > 1:
|
|
86
|
+
location["catalog"] = outer[-2]
|
|
87
|
+
return Upstream(asset=upstream.asset, data=ibis.table(ibis_schema(reference.schema), name=table, **location))
|
|
88
|
+
|
|
89
|
+
def _output(self, result: Any) -> Output:
|
|
102
90
|
"""Check the expression against the declared schema and wrap it.
|
|
103
91
|
|
|
104
92
|
Args:
|
|
105
93
|
result: The value ``data()`` returned.
|
|
106
94
|
|
|
107
95
|
Returns:
|
|
108
|
-
The compilable definition.
|
|
96
|
+
The output, its data the compilable definition.
|
|
109
97
|
|
|
110
98
|
Raises:
|
|
111
99
|
ConfigError: If the result is not an Ibis table, the view
|
|
@@ -126,7 +114,7 @@ class IbisView(View):
|
|
|
126
114
|
f"View '{self.qualified_key}' declares columns its expression does not produce: "
|
|
127
115
|
f"missing {missing}, extra {extra}, retyped {retyped}"
|
|
128
116
|
)
|
|
129
|
-
return IbisDefinition(result)
|
|
117
|
+
return Output(IbisDefinition(result))
|
|
130
118
|
|
|
131
119
|
|
|
132
120
|
def union(upstreams: Sequence[Upstream], *columns: str, **stamps: Callable[[Upstream], Any]) -> ibis.Table:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The ibis_view decorator: an IbisView subclass from a decorated function."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from interloper.component import Decorator
|
|
6
|
+
|
|
7
|
+
from interloper_sql.view.base import IbisView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IbisViewDecorator(Decorator[IbisView]):
|
|
11
|
+
"""Create an IbisView subclass from a decorated function.
|
|
12
|
+
|
|
13
|
+
Like ``il.view``, but portable: each upstream parameter receives a typed
|
|
14
|
+
``ibis.Table`` in ``data``, the function returns an Ibis expression, and the
|
|
15
|
+
destination compiles it for its own dialect:
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
@ibis_view(schema=Totals, relations={"orders": il.Relation("asset", "shop.orders", many=True)})
|
|
19
|
+
def totals(orders: list[il.Upstream]) -> ibis.Table:
|
|
20
|
+
rows = union(orders)
|
|
21
|
+
return rows.group_by(rows.date).aggregate(total=rows.total.sum())
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It takes the settings ``il.asset`` takes (a schema is required) and returns an
|
|
25
|
+
IbisView subclass with the function as its ``data()``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ibis_view = IbisViewDecorator(IbisView)
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""Decorator for creating IbisView subclasses from functions."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from typing import Any, cast, overload
|
|
7
|
-
|
|
8
|
-
from interloper.asset.decorator import build_asset_class
|
|
9
|
-
from interloper.component.decorator import decorate
|
|
10
|
-
from interloper.component.relation import Relation
|
|
11
|
-
|
|
12
|
-
from interloper_sql.view.base import IbisView
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@overload
|
|
16
|
-
def ibis_view(fn: Callable[..., Any], /) -> type[IbisView]: ...
|
|
17
|
-
@overload
|
|
18
|
-
def ibis_view(
|
|
19
|
-
*,
|
|
20
|
-
relations: dict[str, Any] = ...,
|
|
21
|
-
**overrides: Any,
|
|
22
|
-
) -> Callable[[Callable[..., Any]], type[IbisView]]: ...
|
|
23
|
-
def ibis_view(
|
|
24
|
-
fn: Callable[..., Any] | None = None,
|
|
25
|
-
/,
|
|
26
|
-
*,
|
|
27
|
-
relations: dict[str, Any] | None = None,
|
|
28
|
-
**overrides: Any,
|
|
29
|
-
) -> type[IbisView] | Callable[..., type[IbisView]]:
|
|
30
|
-
"""Create an IbisView subclass from a decorated function.
|
|
31
|
-
|
|
32
|
-
Like ``il.view``, but portable: each upstream parameter receives a typed
|
|
33
|
-
``ibis.Table`` in ``data``, the function returns an Ibis expression, and
|
|
34
|
-
the destination compiles it for its own dialect:
|
|
35
|
-
|
|
36
|
-
```py
|
|
37
|
-
@ibis_view(schema=Totals, relations={"orders": il.Relation("asset", "shop.orders", many=True)})
|
|
38
|
-
def totals(orders: list[il.Upstream]) -> ibis.Table:
|
|
39
|
-
rows = union(orders)
|
|
40
|
-
return rows.group_by(rows.date).aggregate(total=rows.total.sum())
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
fn: The function to turn into a view, passed positionally when the
|
|
45
|
-
decorator is used bare. ``None`` in the parenthesised form.
|
|
46
|
-
relations: As for ``il.asset``.
|
|
47
|
-
**overrides: As for ``il.asset``; a schema is required.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
An IbisView subclass with the function as its ``data()`` method.
|
|
51
|
-
"""
|
|
52
|
-
if fn is not None:
|
|
53
|
-
return decorate(IbisView, fn, build=_build_ibis_view_class, relations=relations, **overrides)
|
|
54
|
-
|
|
55
|
-
def wrapper(fn: Callable[..., Any]) -> type[IbisView]:
|
|
56
|
-
return decorate(IbisView, fn, build=_build_ibis_view_class, relations=relations, **overrides)
|
|
57
|
-
|
|
58
|
-
return wrapper
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# -- Internals -----------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _build_ibis_view_class(
|
|
65
|
-
fn: Any,
|
|
66
|
-
*,
|
|
67
|
-
classvars: dict[str, Any],
|
|
68
|
-
fields: dict[str, Any],
|
|
69
|
-
relations: dict[str, Relation],
|
|
70
|
-
) -> type[IbisView]:
|
|
71
|
-
"""Build an IbisView subclass from a function or method.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
fn: The function (or method) backing the view's ``data()``.
|
|
75
|
-
classvars: Class-level attributes to set on the generated subclass.
|
|
76
|
-
fields: Pydantic field values to set on the generated subclass.
|
|
77
|
-
relations: Relations the decorator declares, name to relation.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
A dynamically created IbisView subclass.
|
|
81
|
-
"""
|
|
82
|
-
return cast(
|
|
83
|
-
"type[IbisView]", build_asset_class(fn, base=IbisView, classvars=classvars, fields=fields, relations=relations)
|
|
84
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|