interloper-sql 0.111.0__tar.gz → 0.114.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-sql
3
- Version: 0.111.0
3
+ Version: 0.114.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.111.0"
3
+ version = "0.114.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.111.0"
6
+ version = "0.114.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.fields import SecretField, fetch_field_provider
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.representation import Representation
17
- from interloper.resource.fields import InputField
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
- view = Representation.of(data)
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 view.infer()
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(view.records, [c.name for c in sql_table.columns], self._ref(table, dataset))
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: equality on ``value``, or half-open ``bounds``.
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.bounds is not None:
322
- start, end = where.bounds
323
- return and_(column >= _coerce(column, start), column < _coerce(column, end))
324
- return column == _coerce(column, where.value)
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 id arrives as a string (``"2024-01-01"``) and a day's bounds
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.database import TableReference
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.dag.base import DAG
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 _read_upstreams(
60
- self,
61
- name: str,
62
- targets: list[Asset],
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 upstreams are built for.
71
- targets: The bound upstream assets.
72
- dag: Forwarded to `View._read_upstreams`.
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
- One `Upstream` per target, its ``data`` an ``ibis.Table``.
70
+ The upstream, its ``data`` an ``ibis.Table``.
78
71
 
79
72
  Raises:
80
- ConfigError: If an upstream declares no schema, or its reference is not a table.
73
+ ConfigError: If the upstream declares no schema, or its reference is not a table.
81
74
  """
82
- upstreams = await super()._read_upstreams(name, targets, dag, partition_or_window, metadata)
83
- tables: list[Upstream] = []
84
- for upstream in upstreams:
85
- reference = upstream.data
86
- if not isinstance(reference, TableReference):
87
- raise ConfigError(f"Upstream '{upstream.asset.qualified_key}' is not a table: {reference!r}")
88
- if reference.schema is None:
89
- raise ConfigError(f"Upstream '{upstream.asset.qualified_key}' declares no schema to type its table")
90
- *outer, table = reference.parts
91
- location = {}
92
- if outer:
93
- location["database"] = outer[-1]
94
- if len(outer) > 1:
95
- location["catalog"] = outer[-2]
96
- tables.append(
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
- )