continuo-validation-trino 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Trino warehouse adapter for the continuo validation runner."""
@@ -0,0 +1,130 @@
1
+ """Trino implementation of the WarehouseAdapter port.
2
+
3
+ Trino's namespace is catalog.schema: the catalog comes from TRINO_CATALOG and every
4
+ statement is fully qualified, so the contract's `schema` argument stays an opaque bare
5
+ name. Two Trino traits shape this adapter: there is no DROP SCHEMA ... CASCADE, so
6
+ teardown drops the schema's tables first; and there are no advisory locks, so
7
+ ensure_schema relies on IF NOT EXISTS plus tolerating a concurrent creation.
8
+ """
9
+ import logging
10
+ import os
11
+
12
+ from continuo_validation_contract.port import WarehouseAdapter
13
+
14
+ import trino
15
+
16
+ from trino.auth import BasicAuthentication
17
+
18
+ logger = logging.getLogger("continuo_validation_trino")
19
+
20
+
21
+ def _quote(identifier: str) -> str:
22
+ """Double-quote a Trino identifier, escaping any embedded double quotes."""
23
+ escaped = identifier.replace('"', '""')
24
+ return f'"{escaped}"'
25
+
26
+
27
+ class TrinoAdapter(WarehouseAdapter):
28
+ """WarehouseAdapter speaking Trino DDL over the trino DBAPI."""
29
+
30
+ def __init__(self, conn: "trino.dbapi.Connection", catalog: str) -> None:
31
+ self._conn = conn
32
+ self._catalog = catalog
33
+
34
+ @classmethod
35
+ def required_env(cls) -> list[str]:
36
+ """Vars that must be non-empty before connecting."""
37
+ return ["TRINO_HOST", "TRINO_CATALOG"]
38
+
39
+ @classmethod
40
+ def from_env(cls) -> "TrinoAdapter":
41
+ """Connect from TRINO_* env (port 8080, user continuo, http; password optional)."""
42
+ catalog = os.environ["TRINO_CATALOG"]
43
+ user = os.environ.get("TRINO_USER", "continuo")
44
+ http_scheme = os.environ.get("TRINO_HTTP_SCHEME", "http")
45
+ password = os.environ.get("TRINO_PASSWORD", "")
46
+ if password and http_scheme != "https":
47
+ raise ValueError(
48
+ "TRINO_PASSWORD is set but TRINO_HTTP_SCHEME is not 'https'; "
49
+ "Trino refuses basic auth over plaintext"
50
+ )
51
+ conn = trino.dbapi.connect(
52
+ host=os.environ["TRINO_HOST"],
53
+ port=int(os.environ.get("TRINO_PORT", "8080")),
54
+ user=user,
55
+ catalog=catalog,
56
+ http_scheme=http_scheme,
57
+ auth=BasicAuthentication(user, password) if password else None,
58
+ )
59
+ return cls(conn, catalog)
60
+
61
+ def _schema_ref(self, schema: str) -> str:
62
+ return f"{_quote(self._catalog)}.{_quote(schema)}"
63
+
64
+ def _table_ref(self, schema: str, table: str) -> str:
65
+ return f"{self._schema_ref(schema)}.{_quote(table)}"
66
+
67
+ def _execute(self, statement: str) -> list[tuple]:
68
+ """Run one statement to completion and return its rows.
69
+
70
+ The trino DBAPI is lazy: execute() only starts the query, so the results must
71
+ be consumed for DDL to actually take effect.
72
+ """
73
+ cur = self._conn.cursor()
74
+ try:
75
+ cur.execute(statement)
76
+ rows: list[tuple] = cur.fetchall()
77
+ return rows
78
+ finally:
79
+ cur.close()
80
+
81
+ def ensure_schema(self, schema: str) -> None:
82
+ """Idempotently create *schema*; safe under concurrent callers."""
83
+ logger.info("ensuring candidate schema %s.%s exists", self._catalog, schema)
84
+ try:
85
+ self._execute(f"CREATE SCHEMA IF NOT EXISTS {self._schema_ref(schema)}")
86
+ except trino.exceptions.TrinoUserError as exc:
87
+ # Trino has no advisory locks; a concurrent creator can still win the race
88
+ # between IF NOT EXISTS and the metastore write. The desired end state holds.
89
+ if "already exists" not in str(exc).lower():
90
+ raise
91
+ logger.info("schema %s already exists (concurrent create); continuing", schema)
92
+
93
+ def drop_schema(self, schema: str) -> None:
94
+ """Idempotently drop *schema* and everything in it; no-op if absent.
95
+
96
+ Trino has no DROP SCHEMA ... CASCADE, so the schema's tables are dropped first.
97
+ """
98
+ ref = self._schema_ref(schema)
99
+ try:
100
+ rows = self._execute(f"SHOW TABLES FROM {ref}")
101
+ except trino.exceptions.TrinoUserError as exc:
102
+ if "does not exist" not in str(exc).lower():
103
+ raise
104
+ logger.info("candidate schema %s absent; nothing to drop", schema)
105
+ return
106
+ logger.info("dropping candidate schema %s (%d tables)", schema, len(rows))
107
+ for row in rows:
108
+ self._execute(f"DROP TABLE IF EXISTS {self._table_ref(schema, row[0])}")
109
+ self._execute(f"DROP SCHEMA IF EXISTS {ref}")
110
+
111
+ def build_empty_from_sql(self, schema: str, table: str, compiled_sql: str) -> None:
112
+ """Create ``schema.table`` empty, shaped by the compiled SELECT."""
113
+ # Strip any trailing terminator so the SELECT nests cleanly inside AS ( ... ).
114
+ inner = compiled_sql.strip().rstrip(";").strip()
115
+ ref = self._table_ref(schema, table)
116
+ self._execute(f"DROP TABLE IF EXISTS {ref}")
117
+ self._execute(f"CREATE TABLE {ref} AS ({inner}) WITH NO DATA")
118
+
119
+ def clone_empty_from_prod(self, candidate_schema: str, prod_schema: str, table: str) -> None:
120
+ """Create ``candidate_schema.table`` empty, shaped like ``prod_schema.table``."""
121
+ ref = self._table_ref(candidate_schema, table)
122
+ self._execute(f"DROP TABLE IF EXISTS {ref}")
123
+ self._execute(
124
+ f"CREATE TABLE {ref} AS "
125
+ f"SELECT * FROM {self._table_ref(prod_schema, table)} WITH NO DATA"
126
+ )
127
+
128
+ def close(self) -> None:
129
+ """Release the underlying connection."""
130
+ self._conn.close()
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: continuo-validation-trino
3
+ Version: 0.1.0
4
+ Summary: Trino warehouse adapter for the continuo validation runner.
5
+ Author: Simone Carolini
6
+ Maintainer: Simone Carolini
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Python: >=3.14
11
+ Requires-Dist: continuo-validation-contract==0.2.0
12
+ Requires-Dist: trino==0.338.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # continuo-validation-trino
16
+
17
+ Trino warehouse adapter for the [continuo](https://github.com/carolsimone/continuo)
18
+ validation runner. Implements `continuo_validation_contract.port.WarehouseAdapter`;
19
+ continuo's harness discovers it through the `continuo_validation.adapters` entry point.
20
+
21
+ Trino's namespace is `catalog.schema`. The catalog comes from `TRINO_CATALOG` and every
22
+ statement is fully qualified, so continuo passes a bare candidate schema name and never
23
+ needs to know about catalogs.
24
+
25
+ ## Connection env
26
+
27
+ | Env | Required | Default | Purpose |
28
+ |---|---|---|---|
29
+ | `TRINO_HOST` | yes | — | Coordinator host |
30
+ | `TRINO_CATALOG` | yes | — | Catalog holding candidate schemas (e.g. `iceberg`) |
31
+ | `TRINO_PORT` | no | `8080` | Coordinator port |
32
+ | `TRINO_USER` | no | `continuo` | Trino user |
33
+ | `TRINO_HTTP_SCHEME` | no | `http` | `http` or `https` |
34
+ | `TRINO_PASSWORD` | no | unset | Basic auth; requires `TRINO_HTTP_SCHEME=https` |
35
+
36
+ The catalog must be backed by a connector that supports schema and table creation
37
+ (Iceberg and Hive do). The library is tested live against Iceberg (REST catalog + S3).
@@ -0,0 +1,6 @@
1
+ continuo_validation_trino/__init__.py,sha256=xN9flSm3q9z3FPg4ol2bqhsPF3CvoHAaw6rkE8fCXa0,66
2
+ continuo_validation_trino/adapter.py,sha256=2zNv2xLf8lbU-ccoUMpueG5gM8Z5w0qDTUlaTcwRjIY,5478
3
+ continuo_validation_trino-0.1.0.dist-info/METADATA,sha256=ySIHFNs5s5uIbFUxnJcYnJ2MSBxO5aGfVV0TKvid2hk,1585
4
+ continuo_validation_trino-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
5
+ continuo_validation_trino-0.1.0.dist-info/entry_points.txt,sha256=2FGDkX5J9yrnyYlmoetrgm59lgMk7JYEq1CJ1jklH4k,86
6
+ continuo_validation_trino-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [continuo_validation.adapters]
2
+ trino = continuo_validation_trino.adapter:TrinoAdapter