ob-postgres 2.1.0__tar.gz → 2.36.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,67 @@
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
+ # Generated FOCUS export (20 MB); rebuilt by scripts/build_finops_duckdb.py.
16
+ # The committed excerpt is examples/finops_charges_sample.json.
17
+ examples/finops_data/
18
+ *.sqlite3
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
22
+ htmlcov/
23
+ .coverage
24
+ .coverage.*
25
+ *.log
26
+ .DS_Store
27
+ .claude/
28
+ # Playwright MCP scratch output: accessibility snapshots and console logs
29
+ # written per browser session. The console logs were already caught by
30
+ # *.log above, which left only the .yml page dumps showing as untracked.
31
+ .playwright-mcp/
32
+ # Playwright writes screenshots to the working directory when the tool is
33
+ # given a bare filename, which is the repo root. Twice now one has been
34
+ # swept into a commit by ``git add -A``.
35
+ *.playwright.png
36
+ gradio*-production.png
37
+ ui-gradio*.png
38
+ /cache/
39
+ /usage/
40
+ security/
41
+ infra/
42
+ site/
43
+ scripts-infra
44
+ .mcpregistry_github_token
45
+ .mcpregistry_registry_token
46
+ _*/
47
+ blog/
48
+ review*
49
+ design/
50
+ examples/test*
51
+ # TPC-DS sweep harness. The queries, the runner and the compiled SQL are
52
+ # published so the coverage is auditable; the generated database, the working
53
+ # notes and the drafts that do not yet match the reference are not.
54
+ examples/tpcds_queries/*
55
+ !examples/tpcds_queries/README.md
56
+ !examples/tpcds_queries/sweep.py
57
+ !examples/tpcds_queries/Q*.yml
58
+ !examples/tpcds_queries/sql/
59
+ examples/tpcds_queries/*.duckdb
60
+ examples/tpcds_queries/REPORT.md
61
+ examples/tpcds_queries/tpcds_run.py
62
+ examples/tpcds_queries/drafts/
63
+ semantic-layer-spec.md
64
+
65
+ # Generated per-vendor seed scripts (backup of what the drift seed executed)
66
+ tests/integration/drift/vendor_exec/seed_sql/
67
+ examples/tpcds_queries/results/
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -3,8 +3,8 @@ Business Source License 1.1
3
3
  Parameters
4
4
 
5
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.
6
+ Licensed Work: OrionBelt Semantic Layer (all versions)
7
+ The Licensed Work is (c) 2025-2026 RALFORION d.o.o.
8
8
  Additional Use Grant: You may use the Licensed Work for any purpose,
9
9
  including production use, PROVIDED THAT you may
10
10
  not embed OrionBelt in a commercial product or
@@ -1,14 +1,14 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: ob-postgres
3
- Version: 2.1.0
3
+ Version: 2.36.0
4
4
  Summary: OrionBelt Semantic Layer driver for PostgreSQL (DB-API 2.0)
5
5
  Author-email: Ralf Becher <info@orionbelt.ai>
6
- License: BSL-1.1
6
+ License-Expression: BUSL-1.1
7
7
  License-File: LICENSE
8
8
  Keywords: db-api,obml,orionbelt,postgres,semantic-layer
9
9
  Requires-Python: >=3.12
10
10
  Requires-Dist: adbc-driver-postgresql>=1.0
11
- Requires-Dist: ob-driver-core>=0.1
11
+ Requires-Dist: ob-driver-core>=2.36.0
12
12
  Requires-Dist: pyarrow>=16.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: mypy>=1.10; extra == 'dev'
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
24
24
 
25
25
  OrionBelt Semantic Layer driver for PostgreSQL (DB-API 2.0)
26
26
 
27
- Part of the [OrionBelt Semantic Layer](https://github.com/ralfbecher/orionbelt-semantic-layer) project.
27
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralforion/orionbelt-semantic-layer) project.
28
28
 
29
29
  ## Installation
30
30
 
@@ -34,4 +34,4 @@ pip install ob-postgres
34
34
 
35
35
  ## License
36
36
 
37
- BSL-1.1 — see [LICENSE](LICENSE) for details.
37
+ BUSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,15 @@
1
+ # ob-postgres
2
+
3
+ OrionBelt Semantic Layer driver for PostgreSQL (DB-API 2.0)
4
+
5
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralforion/orionbelt-semantic-layer) project.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install ob-postgres
11
+ ```
12
+
13
+ ## License
14
+
15
+ BUSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -4,15 +4,16 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ob-postgres"
7
- version = "2.1.0"
7
+ version = "2.36.0"
8
8
  description = "OrionBelt Semantic Layer driver for PostgreSQL (DB-API 2.0)"
9
- license = { text = "BSL-1.1" }
9
+ license = "BUSL-1.1"
10
+ license-files = ["LICENSE"]
10
11
  readme = "README.md"
11
12
  authors = [{ name = "Ralf Becher", email = "info@orionbelt.ai" }]
12
13
  keywords = ["postgres", "semantic-layer", "obml", "db-api", "orionbelt"]
13
14
  requires-python = ">=3.12"
14
15
  dependencies = [
15
- "ob-driver-core>=0.1",
16
+ "ob-driver-core>=2.36.0",
16
17
  "adbc-driver-postgresql>=1.0",
17
18
  "pyarrow>=16.0",
18
19
  ]
@@ -39,12 +40,28 @@ target-version = "py312"
39
40
 
40
41
  [tool.ruff.lint]
41
42
  select = ["E", "F", "I", "UP", "B", "SIM", "ANN"]
43
+ ignore = [
44
+ # PEP 249 is an untyped boundary by specification: ``execute()`` takes
45
+ # whatever the caller passes, and every cursor wraps a native object no
46
+ # vendor annotates. ``Any`` there is the honest signature, not a missing one.
47
+ "ANN401",
48
+ ]
49
+
50
+ [tool.ruff.lint.per-file-ignores]
51
+ # A test reads better with its patches nested, and its return type says nothing
52
+ # a reader needs.
53
+ "tests/**" = ["ANN001", "ANN201", "SIM117"]
42
54
 
43
55
  [tool.mypy]
44
56
  python_version = "3.12"
45
57
  strict = true
46
58
  files = ["src/ob_postgres"]
47
59
 
60
+ # Untyped upstream - neither ships a py.typed marker.
61
+ [[tool.mypy.overrides]]
62
+ module = ["pyarrow.*", "adbc_driver_postgresql.*"]
63
+ ignore_missing_imports = true
64
+
48
65
  [tool.pytest.ini_options]
49
66
  asyncio_mode = "auto"
50
67
  testpaths = ["tests"]
@@ -11,7 +11,7 @@ from typing import Any
11
11
 
12
12
  from ob_postgres.compiler import compile_obml, is_obml, parse_obml
13
13
  from ob_postgres.exceptions import NotSupportedError, ProgrammingError
14
- from ob_postgres.type_codes import PG_OID_MAP, STRING
14
+ from ob_postgres.type_codes import BINARY, DATETIME, NUMBER, PG_OID_MAP, STRING
15
15
 
16
16
 
17
17
  class Cursor:
@@ -42,7 +42,14 @@ class Cursor:
42
42
  def description(
43
43
  self,
44
44
  ) -> tuple[tuple[str, Any, None, None, None, None, None], ...] | None:
45
- """PEP 249 cursor description — 7-item tuples per column."""
45
+ """PEP 249 cursor description — 7-item tuples per column.
46
+
47
+ ``adbc-driver-postgresql`` returns PyArrow ``DataType`` objects
48
+ (or ``OpaqueType`` for vendor-specific types like ``numeric``)
49
+ as the ``type_code`` slot. psycopg2 returns OID integers. We
50
+ handle both so downstream type-hint mapping (``_map_type_code``
51
+ in db_executor.py) sees the right PEP 249 type constant.
52
+ """
46
53
  native_desc = self._native.description
47
54
  if native_desc is None:
48
55
  return None
@@ -50,14 +57,20 @@ class Cursor:
50
57
  for col in native_desc:
51
58
  name = col[0]
52
59
  type_code = col[1]
53
- # ADBC PG driver may return OIDs or other type identifiers
54
- mapped = PG_OID_MAP.get(type_code, STRING) if isinstance(type_code, int) else STRING
60
+ mapped = _classify_type_code(type_code)
55
61
  cols.append((name, mapped, None, None, None, None, None))
56
62
  return tuple(cols)
57
63
 
58
64
  @property
59
65
  def rowcount(self) -> int:
60
- return self._native.rowcount
66
+ """Rows the last statement produced, or -1 where the driver says nothing.
67
+
68
+ ``self._native`` is untyped, so the value arrives as ``Any``; PEP 249
69
+ promises an ``int`` and -1 is what it reserves for "unknown", which is
70
+ what a connector answering ``None`` means.
71
+ """
72
+ count = self._native.rowcount
73
+ return int(count) if isinstance(count, int) else -1
61
74
 
62
75
  def _check_open(self) -> None:
63
76
  if self._closed:
@@ -156,3 +169,76 @@ class Cursor:
156
169
  if row is None:
157
170
  raise StopIteration
158
171
  return row
172
+
173
+
174
+ # Substrings inside an OpaqueType's repr that identify the underlying
175
+ # Postgres type. ADBC wraps types it can't represent natively (NUMERIC,
176
+ # MONEY, JSON, …) in ``OpaqueType`` whose repr embeds ``type_name=<pg>``.
177
+ _OPAQUE_NUMBER_NAMES = ("numeric", "money", "decimal")
178
+ _OPAQUE_DATETIME_NAMES = ("timestamp", "date", "time", "interval")
179
+ _OPAQUE_BINARY_NAMES = ("bytea",)
180
+
181
+
182
+ def _classify_type_code(type_code: object) -> object:
183
+ """Map a native ADBC type identifier to a PEP 249 type constant.
184
+
185
+ ADBC's PostgreSQL driver populates ``cursor.description[i][1]``
186
+ with PyArrow ``DataType`` objects — ``DataType(int32)``,
187
+ ``TimestampType(timestamp[us, tz=UTC])``, ``DataType(string)`` —
188
+ plus ``OpaqueType`` wrappers for types Arrow can't natively
189
+ represent (NUMERIC ends up as
190
+ ``OpaqueType(extension<arrow.opaque[storage_type=string,
191
+ type_name=numeric, vendor_name=PostgreSQL]>)``).
192
+ The legacy psycopg2 path supplies OID integers, kept for
193
+ compatibility.
194
+ """
195
+
196
+ # psycopg2 / classic PEP 249 — bare OID.
197
+ if isinstance(type_code, int):
198
+ return PG_OID_MAP.get(type_code, STRING)
199
+
200
+ # ADBC native PyArrow types — use pyarrow.types helpers when the
201
+ # module is importable. ``pa.types.is_*`` calls ``t.id`` internally,
202
+ # so an unrelated object (OpaqueType subclass, mock, etc.) trips
203
+ # ``AttributeError`` — catch that and fall through to the
204
+ # repr-substring branch below.
205
+ try:
206
+ import pyarrow as pa
207
+
208
+ try:
209
+ if pa.types.is_integer(type_code):
210
+ return NUMBER
211
+ if pa.types.is_floating(type_code) or pa.types.is_decimal(type_code):
212
+ return NUMBER
213
+ if pa.types.is_boolean(type_code):
214
+ return STRING
215
+ if (
216
+ pa.types.is_timestamp(type_code)
217
+ or pa.types.is_date(type_code)
218
+ or pa.types.is_time(type_code)
219
+ or pa.types.is_duration(type_code)
220
+ ):
221
+ return DATETIME
222
+ if (
223
+ pa.types.is_binary(type_code)
224
+ or pa.types.is_large_binary(type_code)
225
+ or pa.types.is_fixed_size_binary(type_code)
226
+ ):
227
+ return BINARY
228
+ if pa.types.is_string(type_code) or pa.types.is_large_string(type_code):
229
+ return STRING
230
+ except (AttributeError, TypeError):
231
+ pass
232
+ except ImportError:
233
+ pass
234
+
235
+ # OpaqueType (NUMERIC, MONEY, JSON, …) — the type's repr embeds
236
+ # ``type_name=<pg type>``. Match against the substring lists.
237
+ s = str(type_code).lower()
238
+ if any(name in s for name in _OPAQUE_NUMBER_NAMES):
239
+ return NUMBER
240
+ if any(name in s for name in _OPAQUE_DATETIME_NAMES):
241
+ return DATETIME
242
+ if any(name in s for name in _OPAQUE_BINARY_NAMES):
243
+ return BINARY
244
+ return STRING
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from ob_driver_core.type_codes import BINARY, DATETIME, NUMBER, ROWID, STRING
10
10
 
11
- __all__ = ["STRING", "BINARY", "NUMBER", "DATETIME", "ROWID", "PG_OID_MAP"]
11
+ __all__ = ["BINARY", "DATETIME", "NUMBER", "PG_OID_MAP", "ROWID", "STRING"]
12
12
 
13
13
  # Common PostgreSQL OIDs — see pg_type catalog
14
14
  # https://www.postgresql.org/docs/current/catalog-pg-type.html
@@ -16,14 +16,15 @@ from ob_postgres.connection import Connection
16
16
  from ob_postgres.exceptions import NotSupportedError, ProgrammingError
17
17
  from ob_postgres.type_codes import DATETIME, NUMBER, STRING
18
18
 
19
-
20
19
  # ---------------------------------------------------------------------------
21
20
  # Helper to build a mock psycopg2 connection
22
21
  # ---------------------------------------------------------------------------
23
22
 
24
23
  # psycopg2 description columns are named tuples
25
- PgColumn = namedtuple("Column", ["name", "type_code", "display_size",
26
- "internal_size", "precision", "scale", "null_ok"])
24
+ PgColumn = namedtuple(
25
+ "Column",
26
+ ["name", "type_code", "display_size", "internal_size", "precision", "scale", "null_ok"],
27
+ )
27
28
 
28
29
 
29
30
  def _make_mock_native() -> MagicMock:
@@ -62,7 +63,10 @@ def test_threadsafety() -> None:
62
63
 
63
64
 
64
65
  def test_paramstyle() -> None:
65
- assert ob_postgres.paramstyle == "format"
66
+ # ADBC uses ``?`` placeholders — paramstyle ``qmark``. The legacy
67
+ # psycopg2-based driver used ``%s`` (``format``); the migration to
68
+ # ``adbc-driver-postgresql`` changed this.
69
+ assert ob_postgres.paramstyle == "qmark"
66
70
 
67
71
 
68
72
  # ---------------------------------------------------------------------------
@@ -71,27 +75,30 @@ def test_paramstyle() -> None:
71
75
 
72
76
 
73
77
  def test_connect_returns_connection() -> None:
74
- with patch("psycopg2.connect") as mock_connect:
78
+ with patch("adbc_driver_postgresql.dbapi.connect") as mock_connect:
75
79
  mock_connect.return_value = _make_mock_native()
76
80
  conn = ob_postgres.connect(dbname="testdb", user="testuser")
77
81
  assert isinstance(conn, Connection)
78
82
  mock_connect.assert_called_once()
79
- kwargs = mock_connect.call_args.kwargs
80
- assert kwargs["dbname"] == "testdb"
81
- assert kwargs["user"] == "testuser"
83
+ # ADBC takes a URI as its first positional arg — the kwargs are
84
+ # encoded into it. Verify both made the trip.
85
+ (uri,) = mock_connect.call_args.args
86
+ assert "testdb" in uri
87
+ assert "testuser" in uri
82
88
 
83
89
 
84
90
  def test_connect_with_dsn() -> None:
85
- with patch("psycopg2.connect") as mock_connect:
91
+ with patch("adbc_driver_postgresql.dbapi.connect") as mock_connect:
86
92
  mock_connect.return_value = _make_mock_native()
87
- conn = ob_postgres.connect(dsn="host=localhost dbname=mydb")
93
+ dsn = "postgresql://localhost:5432/mydb"
94
+ conn = ob_postgres.connect(dsn=dsn)
88
95
  assert isinstance(conn, Connection)
89
- kwargs = mock_connect.call_args.kwargs
90
- assert kwargs["dsn"] == "host=localhost dbname=mydb"
96
+ (uri,) = mock_connect.call_args.args
97
+ assert uri == dsn
91
98
 
92
99
 
93
100
  def test_connect_context_manager() -> None:
94
- with patch("psycopg2.connect") as mock_connect:
101
+ with patch("adbc_driver_postgresql.dbapi.connect") as mock_connect:
95
102
  mock_native = _make_mock_native()
96
103
  mock_connect.return_value = mock_native
97
104
  with ob_postgres.connect() as conn:
@@ -167,8 +174,8 @@ def test_cursor_description() -> None:
167
174
  mock_native = _make_mock_native()
168
175
  mock_cursor = mock_native.cursor()
169
176
  mock_cursor.description = [
170
- PgColumn("num", 23, None, None, None, None, None), # int4 → NUMBER
171
- PgColumn("txt", 25, None, None, None, None, None), # text → STRING
177
+ PgColumn("num", 23, None, None, None, None, None), # int4 → NUMBER
178
+ PgColumn("txt", 25, None, None, None, None, None), # text → STRING
172
179
  PgColumn("dt", 1114, None, None, None, None, None), # timestamp → DATETIME
173
180
  ]
174
181
  conn = Connection(mock_native)
@@ -185,6 +192,63 @@ def test_cursor_description() -> None:
185
192
  assert desc[2][1] == DATETIME
186
193
 
187
194
 
195
+ def test_cursor_description_classifies_arrow_datatypes() -> None:
196
+ """ADBC returns PyArrow ``DataType`` objects, not OIDs.
197
+
198
+ The legacy code path checked ``isinstance(type_code, int)`` and
199
+ fell back to ``STRING`` for everything else — which caused every
200
+ column from a live ADBC PostgreSQL connection to surface as TEXT,
201
+ breaking Tableau (NUMERIC measures decoded as strings → SUM = 0).
202
+ """
203
+
204
+ import pyarrow as pa
205
+
206
+ mock_native = _make_mock_native()
207
+ mock_cursor = mock_native.cursor()
208
+ mock_cursor.description = [
209
+ PgColumn("a", pa.int32(), None, None, None, None, None),
210
+ PgColumn("b", pa.float64(), None, None, None, None, None),
211
+ PgColumn("c", pa.string(), None, None, None, None, None),
212
+ PgColumn("d", pa.timestamp("us"), None, None, None, None, None),
213
+ PgColumn("e", pa.binary(), None, None, None, None, None),
214
+ ]
215
+ conn = Connection(mock_native)
216
+ cur = conn.cursor()
217
+ desc = cur.description
218
+ assert desc is not None
219
+ assert desc[0][1] == NUMBER # int32
220
+ assert desc[1][1] == NUMBER # float64
221
+ assert desc[2][1] == STRING # string
222
+ assert desc[3][1] == DATETIME # timestamp
223
+ from ob_postgres.type_codes import BINARY
224
+
225
+ assert desc[4][1] == BINARY # binary
226
+
227
+
228
+ def test_cursor_description_classifies_opaque_numeric() -> None:
229
+ """ADBC wraps Postgres NUMERIC in an OpaqueType — repr contains ``type_name=numeric``.
230
+
231
+ Falls through pa.types.is_decimal() (which only matches Arrow
232
+ DecimalType) and into the OpaqueType repr-substring branch.
233
+ """
234
+
235
+ class _FakeOpaque:
236
+ def __repr__(self) -> str:
237
+ return (
238
+ "OpaqueType(extension<arrow.opaque[storage_type=string, "
239
+ "type_name=numeric, vendor_name=PostgreSQL]>)"
240
+ )
241
+
242
+ mock_native = _make_mock_native()
243
+ mock_native.cursor().description = [
244
+ PgColumn("total_sales", _FakeOpaque(), None, None, None, None, None),
245
+ ]
246
+ cur = Connection(mock_native).cursor()
247
+ desc = cur.description
248
+ assert desc is not None
249
+ assert desc[0][1] == NUMBER
250
+
251
+
188
252
  def test_cursor_description_none_before_execute() -> None:
189
253
  mock_native = _make_mock_native()
190
254
  mock_native.cursor().description = None
@@ -309,7 +373,9 @@ def test_obml_compile_and_execute() -> None:
309
373
  PgColumn("revenue", 701, None, None, None, None, None),
310
374
  ]
311
375
  mock_native.cursor().fetchall.return_value = [
312
- ("EMEA", 300.0), ("APAC", 150.0), ("AMER", 550.0),
376
+ ("EMEA", 300.0),
377
+ ("APAC", 150.0),
378
+ ("AMER", 550.0),
313
379
  ]
314
380
  conn = Connection(mock_native)
315
381
  with patch("httpx.post", return_value=_mock_api_response(compiled_sql)):
@@ -350,8 +416,7 @@ def test_plain_sql_passthrough() -> None:
350
416
  """Plain SQL is passed through without OBML compilation — no REST call."""
351
417
  mock_native = _make_mock_native()
352
418
  conn = Connection(mock_native)
353
- with patch("httpx.post") as mock_post:
354
- with conn.cursor() as cur:
355
- cur.execute("SELECT COUNT(*) FROM orders")
356
- mock_post.assert_not_called()
357
- mock_native.cursor().execute.assert_called_once_with("SELECT COUNT(*) FROM orders")
419
+ with patch("httpx.post") as mock_post, conn.cursor() as cur:
420
+ cur.execute("SELECT COUNT(*) FROM orders")
421
+ mock_post.assert_not_called()
422
+ mock_native.cursor().execute.assert_called_once_with("SELECT COUNT(*) FROM orders")
@@ -0,0 +1,34 @@
1
+ """Unit tests for OBML YAML detection — no external services required."""
2
+
3
+ import pytest
4
+
5
+ from ob_postgres.compiler import is_obml, parse_obml
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "query,expected",
10
+ [
11
+ ("select:\n dimensions:\n - Region\n measures:\n - Revenue\n", True),
12
+ ("select:\n measures:\n - Revenue\n", True),
13
+ ("select:\n dimensions:\n - Country\n", True),
14
+ ("SELECT:\n dimensions:\n - X\n", True),
15
+ (" select:\n dimensions:\n - X\n", True),
16
+ ("SELECT * FROM orders", False),
17
+ ("SELECT id\nFROM customers WHERE id = 1", False),
18
+ ("", False),
19
+ ("model:\n name: test\n", False),
20
+ ("select:\n filters:\n - x = 1\n", False),
21
+ ("select:\n - [unclosed", False),
22
+ ("select:\n - dimension1\n", False),
23
+ ],
24
+ )
25
+ def test_obml_detection(query, expected):
26
+ assert is_obml(query) == expected
27
+
28
+
29
+ def test_parse_obml_returns_dict():
30
+ q = "select:\n dimensions:\n - Region\n measures:\n - Revenue\nlimit: 100\n"
31
+ result = parse_obml(q)
32
+ assert result["select"]["dimensions"] == ["Region"]
33
+ assert result["select"]["measures"] == ["Revenue"]
34
+ assert result["limit"] == 100
@@ -1,37 +0,0 @@
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*
36
- examples/tpcds_queries/
37
- semantic-layer-spec.md
@@ -1,15 +0,0 @@
1
- # ob-postgres
2
-
3
- OrionBelt Semantic Layer driver for PostgreSQL (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-postgres
11
- ```
12
-
13
- ## License
14
-
15
- BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -1,29 +0,0 @@
1
- """Unit tests for OBML YAML detection — no external services required."""
2
- import pytest
3
- from ob_postgres.compiler import is_obml, parse_obml
4
-
5
-
6
- @pytest.mark.parametrize("query,expected", [
7
- ("select:\n dimensions:\n - Region\n measures:\n - Revenue\n", True),
8
- ("select:\n measures:\n - Revenue\n", True),
9
- ("select:\n dimensions:\n - Country\n", True),
10
- ("SELECT:\n dimensions:\n - X\n", True),
11
- (" select:\n dimensions:\n - X\n", True),
12
- ("SELECT * FROM orders", False),
13
- ("SELECT id\nFROM customers WHERE id = 1", False),
14
- ("", False),
15
- ("model:\n name: test\n", False),
16
- ("select:\n filters:\n - x = 1\n", False),
17
- ("select:\n - [unclosed", False),
18
- ("select:\n - dimension1\n", False),
19
- ])
20
- def test_obml_detection(query, expected):
21
- assert is_obml(query) == expected
22
-
23
-
24
- def test_parse_obml_returns_dict():
25
- q = "select:\n dimensions:\n - Region\n measures:\n - Revenue\nlimit: 100\n"
26
- result = parse_obml(q)
27
- assert result["select"]["dimensions"] == ["Region"]
28
- assert result["select"]["measures"] == ["Revenue"]
29
- assert result["limit"] == 100
@@ -139,19 +139,19 @@ def connect(
139
139
 
140
140
 
141
141
  __all__ = [
142
- "apilevel",
143
- "threadsafety",
144
- "paramstyle",
145
- "connect",
146
142
  "Connection",
147
- "Warning",
148
- "Error",
149
- "InterfaceError",
150
- "DatabaseError",
151
143
  "DataError",
152
- "OperationalError",
144
+ "DatabaseError",
145
+ "Error",
153
146
  "IntegrityError",
147
+ "InterfaceError",
154
148
  "InternalError",
155
- "ProgrammingError",
156
149
  "NotSupportedError",
150
+ "OperationalError",
151
+ "ProgrammingError",
152
+ "Warning",
153
+ "apilevel",
154
+ "connect",
155
+ "paramstyle",
156
+ "threadsafety",
157
157
  ]
@@ -6,8 +6,8 @@ Allows users to do: ``from ob_postgres.exceptions import ProgrammingError``
6
6
  from __future__ import annotations
7
7
 
8
8
  from ob_driver_core.exceptions import (
9
- DataError,
10
9
  DatabaseError,
10
+ DataError,
11
11
  Error,
12
12
  IntegrityError,
13
13
  InterfaceError,
@@ -19,14 +19,14 @@ from ob_driver_core.exceptions import (
19
19
  )
20
20
 
21
21
  __all__ = [
22
- "Warning",
23
- "Error",
24
- "InterfaceError",
25
- "DatabaseError",
26
22
  "DataError",
27
- "OperationalError",
23
+ "DatabaseError",
24
+ "Error",
28
25
  "IntegrityError",
26
+ "InterfaceError",
29
27
  "InternalError",
30
- "ProgrammingError",
31
28
  "NotSupportedError",
29
+ "OperationalError",
30
+ "ProgrammingError",
31
+ "Warning",
32
32
  ]