ob-clickhouse 0.1.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.
@@ -0,0 +1,35 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ .env
13
+ *.db
14
+ *.duckdb
15
+ *.sqlite3
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ *.log
23
+ .DS_Store
24
+ .claude/
25
+ security/
26
+ infra/
27
+ site/
28
+ scripts/
29
+ .mcpregistry_github_token
30
+ .mcpregistry_registry_token
31
+ _*/
32
+ blog/
33
+ review*
34
+ design/
35
+ examples/test*
@@ -0,0 +1,105 @@
1
+ # ob-clickhouse — OrionBelt Semantic Layer Driver for Clickhouse
2
+
3
+ ## Purpose
4
+
5
+ PEP 249 DB-API 2.0 driver wrapping `clickhouse-connect = ">=0.7"` that intercepts OBML YAML
6
+ queries, compiles them to clickhouse SQL via the OrionBelt CompilationPipeline
7
+ (direct import) or OB REST API (standalone), and executes them natively.
8
+
9
+ **OB dialect string:** `"clickhouse"`
10
+ **Author:** Ralf Becher / RALFORION d.o.o. (info@orionbelt.ai)
11
+ **License:** Apache 2.0
12
+
13
+ ---
14
+
15
+ ## Module Map
16
+
17
+ | File | Responsibility |
18
+ |---|---|
19
+ | `__init__.py` | PEP 249 connect() + module constants |
20
+ | `connection.py` | Connection class |
21
+ | `cursor.py` | Cursor class — OBML detection + execution |
22
+ | `compiler.py` | Direct OB import or REST fallback |
23
+ | `exceptions.py` | PEP 249 exception hierarchy |
24
+ | `type_codes.py` | PEP 249 type objects |
25
+
26
+ ---
27
+
28
+ ## connect() Parameters
29
+
30
+ ### clickhouse-specific
31
+ | host | str | ClickHouse host |
32
+ | port | int | HTTP port (default: 8123) or native (9000) |
33
+ | username | str | Username (default: default) |
34
+ | password | str | Password |
35
+ | database | str | Database name |
36
+ | secure | bool | Use HTTPS (default: False) |
37
+
38
+ ### OrionBelt parameters (same across all vendors)
39
+ | Parameter | Default | Description |
40
+ |---|---|---|
41
+ | ob_model_id | None | Pre-loaded OB model ID |
42
+ | ob_model_yaml | None | OBML YAML string to load |
43
+ | ob_model_file | None | Path to .obml.yaml file |
44
+ | ob_api_url | http://localhost:8000 | OB REST API URL (standalone mode only) |
45
+ | ob_timeout | 30 | HTTP timeout in seconds |
46
+
47
+ ---
48
+
49
+ ## Compiler Bridge (compiler.py)
50
+
51
+ Auto-detects whether OB core is importable (same process as orionbelt-api)
52
+ or whether to fall back to REST:
53
+
54
+ ```python
55
+ def compile_obml(obml: dict, model, dialect: str) -> str:
56
+ try:
57
+ # Direct call — zero overhead, used when embedded in orionbelt-api
58
+ from orionbelt.compiler.pipeline import CompilationPipeline
59
+ result = CompilationPipeline().compile(obml, model, dialect)
60
+ return result.sql
61
+ except ImportError:
62
+ # Standalone mode — call REST API
63
+ return _compile_via_rest(obml, dialect)
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Vendor-Specific Notes
69
+
70
+ - ClickHouse has no transactions; commit() and rollback() are no-ops
71
+ - Use clickhouse_connect.get_client() not legacy clickhouse-driver
72
+ - FixedString padding: strip trailing null bytes in _post_process_rows()
73
+ - OB dialect "clickhouse" generates toStartOfMonth() etc. for time grains
74
+ - Arrays and Maps are not supported in OBML measures — raise DataError if encountered
75
+
76
+ ---
77
+
78
+ ## Type System
79
+
80
+ clickhouse-connect returns Python native types.
81
+ Map: Int32/64→int, Float32/64→float, String→str, DateTime→datetime.
82
+ FixedString columns require TRIM() — add post-processing in cursor.fetchall().
83
+
84
+ ---
85
+
86
+ ## Build Order
87
+
88
+ Session 1: exceptions.py + type_codes.py + compiler.py + unit tests
89
+ Session 2: connection.py + cursor.py (OBML detection, core execute)
90
+ Session 3: __init__.py + connect() + integration tests
91
+ Session 4: SQLAlchemy dialect (ob+clickhouse:// URL scheme)
92
+
93
+ ---
94
+
95
+ ## Dependencies
96
+
97
+ ```toml
98
+ [project.dependencies]
99
+ clickhouse-connect = ">=0.7"
100
+ pyyaml = ">=6.0"
101
+
102
+ [project.optional-dependencies]
103
+ sqlalchemy = ["sqlalchemy>=2.0"]
104
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "mypy>=1.10", "ruff>=0.4", "respx>=0.21"]
105
+ ```
@@ -0,0 +1,70 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: RALFORION d.o.o.
6
+ Licensed Work: OrionBelt Semantic Layer 1.0
7
+ The Licensed Work is (c) 2025 RALFORION d.o.o.
8
+ Additional Use Grant: You may use the Licensed Work for any purpose,
9
+ including production use, PROVIDED THAT you may
10
+ not embed OrionBelt in a commercial product or
11
+ provide it as part of a commercial service
12
+ without purchasing a commercial license from
13
+ the Licensor. Contact licensing@ralforion.com
14
+ for commercial licensing inquiries.
15
+ Change Date: 2030-03-16
16
+ Change License: Apache License, Version 2.0
17
+
18
+ -----------------------------------------------------------------------------
19
+
20
+ Business Source License 1.1
21
+
22
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
23
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
24
+
25
+ Terms
26
+
27
+ The Licensor hereby grants you the right to copy, modify, create derivative
28
+ works, redistribute, and make non-production use of the Licensed Work. The
29
+ Licensor may make an Additional Use Grant, above, permitting limited
30
+ production use.
31
+
32
+ Effective on the Change Date, or the fourth anniversary of the first publicly
33
+ available distribution of a specific version of the Licensed Work under this
34
+ License, whichever comes first, the Licensor hereby grants you rights under
35
+ the terms of the Change License, and the rights granted in the paragraph
36
+ above terminate.
37
+
38
+ If your use of the Licensed Work does not comply with the requirements
39
+ currently in effect as described in this License, you must purchase a
40
+ commercial license from the Licensor, its affiliated entities, or authorized
41
+ resellers, or you must refrain from using the Licensed Work.
42
+
43
+ All copies of the original and modified Licensed Work, and derivative works
44
+ of the Licensed Work, are subject to this License. This License applies
45
+ separately for each version of the Licensed Work and the Change Date may vary
46
+ for each version of the Licensed Work released by Licensor.
47
+
48
+ You must conspicuously display this License on each original or modified copy
49
+ of the Licensed Work. If you receive the Licensed Work in original or
50
+ modified form from a third party, the terms and conditions set forth in this
51
+ License apply to your use of that work.
52
+
53
+ Any use of the Licensed Work in violation of this License will automatically
54
+ terminate your rights under this License for the current and all other
55
+ versions of the Licensed Work.
56
+
57
+ This License does not grant you any right in any trademark or logo of
58
+ Licensor or its affiliates (provided that you may use a trademark or logo of
59
+ Licensor as expressly required by this License).
60
+
61
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
62
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
63
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
65
+ TITLE.
66
+
67
+ MariaDB hereby grants you permission to use this License's text to license
68
+ your works, and to refer to it using the trademark "Business Source License",
69
+ as long as you comply with the Covenants of Licensor of MariaDB's Business Source
70
+ License Trademark Policy available at https://mariadb.com/bsl11/.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: ob-clickhouse
3
+ Version: 0.1.0
4
+ Summary: OrionBelt Semantic Layer driver for ClickHouse (DB-API 2.0)
5
+ Author-email: Ralf Becher <info@orionbelt.ai>
6
+ License: BSL-1.1
7
+ License-File: LICENSE
8
+ Keywords: clickhouse,db-api,obml,orionbelt,semantic-layer
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: clickhouse-connect>=0.7
11
+ Requires-Dist: ob-driver-core>=0.1
12
+ Requires-Dist: pyarrow>=16.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: respx>=0.21; extra == 'dev'
18
+ Requires-Dist: ruff>=0.4; extra == 'dev'
19
+ Provides-Extra: sqlalchemy
20
+ Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ob-clickhouse
24
+
25
+ OrionBelt Semantic Layer driver for ClickHouse (DB-API 2.0)
26
+
27
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralfbecher/orionbelt-semantic-layer) project.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install ob-clickhouse
33
+ ```
34
+
35
+ ## License
36
+
37
+ BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,15 @@
1
+ # ob-clickhouse
2
+
3
+ OrionBelt Semantic Layer driver for ClickHouse (DB-API 2.0)
4
+
5
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralfbecher/orionbelt-semantic-layer) project.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install ob-clickhouse
11
+ ```
12
+
13
+ ## License
14
+
15
+ BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ob-clickhouse"
7
+ version = "0.1.0"
8
+ description = "OrionBelt Semantic Layer driver for ClickHouse (DB-API 2.0)"
9
+ license = { text = "BSL-1.1" }
10
+ readme = "README.md"
11
+ authors = [{ name = "Ralf Becher", email = "info@orionbelt.ai" }]
12
+ keywords = ["clickhouse", "semantic-layer", "obml", "db-api", "orionbelt"]
13
+ requires-python = ">=3.12"
14
+ dependencies = [
15
+ "ob-driver-core>=0.1",
16
+ "clickhouse-connect>=0.7",
17
+ "pyarrow>=16.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ sqlalchemy = ["sqlalchemy>=2.0"]
22
+ dev = [
23
+ "pytest>=8.0",
24
+ "pytest-asyncio>=0.23",
25
+ "mypy>=1.10",
26
+ "ruff>=0.4",
27
+ "respx>=0.21",
28
+ ]
29
+
30
+ [project.entry-points."sqlalchemy.dialects"]
31
+ "ob.clickhouse" = "ob_clickhouse.dialect:OBClickHouseDialect"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/ob_clickhouse"]
35
+
36
+ [tool.ruff]
37
+ line-length = 100
38
+ target-version = "py312"
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I", "UP", "B", "SIM", "ANN"]
42
+
43
+ [tool.mypy]
44
+ python_version = "3.12"
45
+ strict = true
46
+ files = ["src/ob_clickhouse"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
51
+ markers = ["integration: requires live DB + OB API"]
52
+ addopts = "-m 'not integration'"
@@ -0,0 +1,114 @@
1
+ """ob-clickhouse — OrionBelt Semantic Layer driver for ClickHouse (PEP 249 DB-API 2.0).
2
+
3
+ Requires the OrionBelt REST API running in single-model mode (MODEL_FILE set).
4
+ OBML queries are compiled to SQL via ``POST /v1/query/sql``.
5
+
6
+ Usage::
7
+
8
+ import ob_clickhouse
9
+
10
+ conn = ob_clickhouse.connect(host="localhost", database="default")
11
+ with conn.cursor() as cur:
12
+ cur.execute("select:\\n dimensions:\\n - Region\\n measures:\\n - Revenue")
13
+ print(cur.fetchall())
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ import clickhouse_connect
21
+
22
+ from ob_clickhouse.connection import Connection
23
+ from ob_clickhouse.exceptions import (
24
+ DataError,
25
+ DatabaseError,
26
+ Error,
27
+ IntegrityError,
28
+ InterfaceError,
29
+ InternalError,
30
+ NotSupportedError,
31
+ OperationalError,
32
+ ProgrammingError,
33
+ Warning,
34
+ )
35
+
36
+ # PEP 249 module-level constants
37
+ apilevel = "2.0"
38
+ threadsafety = 1 # threads may share the module but not connections
39
+ paramstyle = "pyformat" # clickhouse-connect uses %(name)s placeholders
40
+
41
+
42
+ def connect(
43
+ *,
44
+ host: str = "localhost",
45
+ port: int = 8123,
46
+ username: str = "default",
47
+ password: str = "",
48
+ database: str = "default",
49
+ secure: bool = False,
50
+ # Extra clickhouse-connect kwargs
51
+ settings: dict[str, Any] | None = None,
52
+ # OrionBelt parameters
53
+ ob_api_url: str = "http://localhost:8000",
54
+ ob_timeout: int = 30,
55
+ ) -> Connection:
56
+ """Open a ClickHouse connection with OBML support.
57
+
58
+ Parameters
59
+ ----------
60
+ host : str
61
+ ClickHouse host (default: ``localhost``).
62
+ port : int
63
+ ClickHouse HTTP port (default: ``8123``).
64
+ username : str
65
+ Username (default: ``default``).
66
+ password : str
67
+ Password (default: empty string).
68
+ database : str
69
+ Database name (default: ``default``).
70
+ secure : bool
71
+ Use HTTPS (default: ``False``).
72
+ settings : dict, optional
73
+ Extra ClickHouse server settings.
74
+ ob_api_url : str
75
+ OrionBelt REST API URL (must be running in single-model mode).
76
+ ob_timeout : int
77
+ HTTP timeout in seconds for OBML compilation.
78
+ """
79
+ client_kwargs: dict[str, Any] = {
80
+ "host": host,
81
+ "port": port,
82
+ "username": username,
83
+ "password": password,
84
+ "database": database,
85
+ "secure": secure,
86
+ }
87
+ if settings:
88
+ client_kwargs["settings"] = settings
89
+
90
+ client = clickhouse_connect.get_client(**client_kwargs)
91
+ return Connection(
92
+ client,
93
+ ob_api_url=ob_api_url,
94
+ ob_timeout=ob_timeout,
95
+ )
96
+
97
+
98
+ __all__ = [
99
+ "apilevel",
100
+ "threadsafety",
101
+ "paramstyle",
102
+ "connect",
103
+ "Connection",
104
+ "Warning",
105
+ "Error",
106
+ "InterfaceError",
107
+ "DatabaseError",
108
+ "DataError",
109
+ "OperationalError",
110
+ "IntegrityError",
111
+ "InternalError",
112
+ "ProgrammingError",
113
+ "NotSupportedError",
114
+ ]
@@ -0,0 +1,8 @@
1
+ """OrionBelt compilation bridge — delegates to ob-driver-core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ob_driver_core.compiler import compile_obml
6
+ from ob_driver_core.detection import is_obml, parse_obml
7
+
8
+ __all__ = ["compile_obml", "is_obml", "parse_obml"]
@@ -0,0 +1,69 @@
1
+ """PEP 249 Connection wrapping a clickhouse-connect ``Client``.
2
+
3
+ ClickHouse has no transactions — ``commit()`` and ``rollback()`` are no-ops
4
+ that simply verify the connection is still open.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from ob_clickhouse.cursor import Cursor
12
+ from ob_clickhouse.exceptions import ProgrammingError
13
+
14
+
15
+ class Connection:
16
+ """DB-API 2.0 connection wrapping a clickhouse-connect Client.
17
+
18
+ OBML queries are compiled to SQL via the OrionBelt REST API
19
+ (single-model mode, ``/v1/query/sql`` shortcut).
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ client: Any,
25
+ *,
26
+ ob_api_url: str = "http://localhost:8000",
27
+ ob_timeout: int = 30,
28
+ ) -> None:
29
+ self._client = client
30
+ self._closed = False
31
+ self._ob_api_url = ob_api_url
32
+ self._ob_timeout = ob_timeout
33
+
34
+ def _check_open(self) -> None:
35
+ if self._closed:
36
+ raise ProgrammingError("Connection is closed.")
37
+
38
+ def cursor(self) -> Cursor:
39
+ """Return a new Cursor backed by the shared Client.
40
+
41
+ clickhouse-connect does not have its own cursor concept — our
42
+ ``Cursor`` wraps the Client directly.
43
+ """
44
+ self._check_open()
45
+ return Cursor(
46
+ self._client,
47
+ ob_api_url=self._ob_api_url,
48
+ ob_timeout=self._ob_timeout,
49
+ )
50
+
51
+ def commit(self) -> None:
52
+ """No-op — ClickHouse has no transactions."""
53
+ self._check_open()
54
+
55
+ def rollback(self) -> None:
56
+ """No-op — ClickHouse has no transactions."""
57
+ self._check_open()
58
+
59
+ def close(self) -> None:
60
+ """Close the connection and the underlying Client."""
61
+ if not self._closed:
62
+ self._client.close()
63
+ self._closed = True
64
+
65
+ def __enter__(self) -> Connection:
66
+ return self
67
+
68
+ def __exit__(self, *exc: object) -> None:
69
+ self.close()