ob-mysql 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.
ob_mysql/__init__.py ADDED
@@ -0,0 +1,133 @@
1
+ """ob-mysql — OrionBelt Semantic Layer driver for MySQL 8.0+ (PEP 249 DB-API 2.0).
2
+
3
+ Uses ``mysql-connector-python`` for database access. OBML queries are compiled
4
+ to MySQL SQL via the OrionBelt REST API running in single-model mode.
5
+
6
+ ``fetch_arrow_table()`` is emulated by fetching rows and converting to a
7
+ PyArrow Table (no native Arrow support in mysql-connector-python).
8
+
9
+ Usage::
10
+
11
+ import ob_mysql
12
+
13
+ conn = ob_mysql.connect(database="mydb", user="me", password="secret")
14
+ with conn.cursor() as cur:
15
+ cur.execute("select:\\n dimensions:\\n - Region\\n measures:\\n - Revenue")
16
+ print(cur.fetchall())
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ import mysql.connector # type: ignore[import-untyped]
24
+
25
+ from ob_mysql.connection import Connection
26
+ from ob_mysql.exceptions import (
27
+ DatabaseError,
28
+ DataError,
29
+ Error,
30
+ IntegrityError,
31
+ InterfaceError,
32
+ InternalError,
33
+ NotSupportedError,
34
+ OperationalError,
35
+ ProgrammingError,
36
+ Warning,
37
+ )
38
+
39
+ # PEP 249 module-level constants
40
+ apilevel = "2.0"
41
+ threadsafety = 1 # threads may share the module but not connections
42
+ paramstyle = "format" # mysql-connector-python uses %s placeholders
43
+
44
+
45
+ def connect(
46
+ dsn: str | None = None,
47
+ *,
48
+ host: str = "localhost",
49
+ port: int = 3306,
50
+ database: str = "mysql",
51
+ user: str | None = None,
52
+ password: str | None = None,
53
+ ssl_ca: str | None = None,
54
+ ssl_cert: str | None = None,
55
+ ssl_key: str | None = None,
56
+ charset: str = "utf8mb4",
57
+ # OrionBelt parameters
58
+ ob_api_url: str = "http://localhost:8000",
59
+ ob_timeout: int = 30,
60
+ ) -> Connection:
61
+ """Open a MySQL connection with OBML support.
62
+
63
+ Parameters
64
+ ----------
65
+ dsn : str, optional
66
+ Not used for MySQL — provided for API consistency with other drivers.
67
+ host : str
68
+ MySQL host (default: ``localhost``).
69
+ port : int
70
+ MySQL port (default: ``3306``).
71
+ database : str
72
+ Database name (default: ``mysql``).
73
+ user : str, optional
74
+ Username.
75
+ password : str, optional
76
+ Password.
77
+ ssl_ca : str, optional
78
+ Path to CA certificate file.
79
+ ssl_cert : str, optional
80
+ Path to client certificate file.
81
+ ssl_key : str, optional
82
+ Path to client key file.
83
+ charset : str
84
+ Character set (default: ``utf8mb4`` for full Unicode support).
85
+ ob_api_url : str
86
+ OrionBelt REST API URL (must be running in single-model mode).
87
+ ob_timeout : int
88
+ HTTP timeout in seconds for OBML compilation.
89
+ """
90
+ kwargs: dict[str, Any] = {
91
+ "host": host,
92
+ "port": port,
93
+ "database": database,
94
+ "charset": charset,
95
+ }
96
+ if user is not None:
97
+ kwargs["user"] = user
98
+ if password is not None:
99
+ kwargs["password"] = password
100
+
101
+ # SSL configuration — only add non-None values
102
+ if ssl_ca:
103
+ kwargs["ssl_ca"] = ssl_ca
104
+ if ssl_cert:
105
+ kwargs["ssl_cert"] = ssl_cert
106
+ if ssl_key:
107
+ kwargs["ssl_key"] = ssl_key
108
+
109
+ native = mysql.connector.connect(**kwargs)
110
+ return Connection(
111
+ native,
112
+ ob_api_url=ob_api_url,
113
+ ob_timeout=ob_timeout,
114
+ )
115
+
116
+
117
+ __all__ = [
118
+ "apilevel",
119
+ "threadsafety",
120
+ "paramstyle",
121
+ "connect",
122
+ "Connection",
123
+ "Warning",
124
+ "Error",
125
+ "InterfaceError",
126
+ "DatabaseError",
127
+ "DataError",
128
+ "OperationalError",
129
+ "IntegrityError",
130
+ "InternalError",
131
+ "ProgrammingError",
132
+ "NotSupportedError",
133
+ ]
ob_mysql/compiler.py ADDED
@@ -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"]
ob_mysql/connection.py ADDED
@@ -0,0 +1,65 @@
1
+ """PEP 249 Connection wrapping ``mysql.connector.connection``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ob_mysql.cursor import Cursor
8
+ from ob_mysql.exceptions import ProgrammingError
9
+
10
+
11
+ class Connection:
12
+ """DB-API 2.0 connection that wraps a mysql-connector-python connection.
13
+
14
+ OBML queries are compiled to MySQL SQL via the OrionBelt REST API.
15
+ ``fetch_arrow_table()`` is emulated by fetching rows and converting
16
+ to a PyArrow Table (mysql-connector-python has no native Arrow support).
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ native: Any,
22
+ *,
23
+ ob_api_url: str = "http://localhost:8000",
24
+ ob_timeout: int = 30,
25
+ ) -> None:
26
+ self._native = native
27
+ self._closed = False
28
+ self._ob_api_url = ob_api_url
29
+ self._ob_timeout = ob_timeout
30
+
31
+ def _check_open(self) -> None:
32
+ if self._closed:
33
+ raise ProgrammingError("Connection is closed.")
34
+
35
+ def cursor(self) -> Cursor:
36
+ """Return a new Cursor for this connection."""
37
+ self._check_open()
38
+ native_cursor = self._native.cursor()
39
+ return Cursor(
40
+ native_cursor,
41
+ ob_api_url=self._ob_api_url,
42
+ ob_timeout=self._ob_timeout,
43
+ )
44
+
45
+ def commit(self) -> None:
46
+ """Commit the current transaction."""
47
+ self._check_open()
48
+ self._native.commit()
49
+
50
+ def rollback(self) -> None:
51
+ """Rollback the current transaction."""
52
+ self._check_open()
53
+ self._native.rollback()
54
+
55
+ def close(self) -> None:
56
+ """Close the connection."""
57
+ if not self._closed:
58
+ self._native.close()
59
+ self._closed = True
60
+
61
+ def __enter__(self) -> Connection:
62
+ return self
63
+
64
+ def __exit__(self, *exc: object) -> None:
65
+ self.close()
ob_mysql/cursor.py ADDED
@@ -0,0 +1,173 @@
1
+ """PEP 249 Cursor with OBML interception for MySQL.
2
+
3
+ Uses ``mysql-connector-python`` for database access. Unlike the ADBC-based
4
+ PostgreSQL driver, ``fetch_arrow_table()`` is emulated by fetching all rows
5
+ via ``fetchall()`` and converting to a PyArrow Table using column metadata.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from ob_mysql.compiler import compile_obml, is_obml, parse_obml
13
+ from ob_mysql.exceptions import NotSupportedError, ProgrammingError
14
+ from ob_mysql.type_codes import MYSQL_TYPE_MAP, STRING
15
+
16
+
17
+ class Cursor:
18
+ """DB-API 2.0 cursor that intercepts OBML YAML queries.
19
+
20
+ If the query is OBML, it is compiled to MySQL SQL via the OrionBelt
21
+ REST API before execution. Plain SQL is passed through unchanged.
22
+
23
+ ``fetch_arrow_table()`` converts row results to a PyArrow Table.
24
+ """
25
+
26
+ arraysize: int = 1
27
+
28
+ def __init__(
29
+ self,
30
+ native: Any,
31
+ *,
32
+ ob_api_url: str = "http://localhost:8000",
33
+ ob_timeout: int = 30,
34
+ ) -> None:
35
+ self._native = native
36
+ self._closed = False
37
+ self._ob_api_url = ob_api_url
38
+ self._ob_timeout = ob_timeout
39
+
40
+ @property
41
+ def description(
42
+ self,
43
+ ) -> tuple[tuple[str, Any, None, None, None, None, None], ...] | None:
44
+ """PEP 249 cursor description — 7-item tuples per column."""
45
+ native_desc = self._native.description
46
+ if native_desc is None:
47
+ return None
48
+ cols: list[tuple[str, Any, None, None, None, None, None]] = []
49
+ for col in native_desc:
50
+ name = col[0]
51
+ type_code = col[1]
52
+ mapped = MYSQL_TYPE_MAP.get(type_code, STRING) if isinstance(type_code, int) else STRING
53
+ cols.append((name, mapped, None, None, None, None, None))
54
+ return tuple(cols)
55
+
56
+ @property
57
+ def rowcount(self) -> int:
58
+ return self._native.rowcount # type: ignore[no-any-return]
59
+
60
+ def _check_open(self) -> None:
61
+ if self._closed:
62
+ raise ProgrammingError("Cursor is closed.")
63
+
64
+ def execute(self, operation: str, parameters: Any = None) -> Cursor:
65
+ """Execute a query — OBML YAML or plain SQL."""
66
+ self._check_open()
67
+ sql = self._resolve_sql(operation)
68
+ if parameters is not None:
69
+ self._native.execute(sql, parameters)
70
+ else:
71
+ self._native.execute(sql)
72
+ return self
73
+
74
+ def executemany(self, operation: str, seq_of_parameters: Any) -> None:
75
+ """Execute against all parameter sequences.
76
+
77
+ OBML queries are not supported with executemany — raises NotSupportedError.
78
+ """
79
+ self._check_open()
80
+ if is_obml(operation):
81
+ raise NotSupportedError("executemany() is not supported for OBML queries.")
82
+ for params in seq_of_parameters:
83
+ self._native.execute(operation, params)
84
+
85
+ def _resolve_sql(self, operation: str) -> str:
86
+ """Compile OBML to SQL or return plain SQL unchanged."""
87
+ if not is_obml(operation):
88
+ return operation
89
+ obml = parse_obml(operation)
90
+ return compile_obml(
91
+ obml,
92
+ dialect="mysql",
93
+ ob_api_url=self._ob_api_url,
94
+ ob_timeout=self._ob_timeout,
95
+ )
96
+
97
+ def fetchone(self) -> tuple[Any, ...] | None:
98
+ """Fetch the next row."""
99
+ self._check_open()
100
+ row = self._native.fetchone()
101
+ return tuple(row) if row is not None else None
102
+
103
+ def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
104
+ """Fetch the next *size* rows."""
105
+ self._check_open()
106
+ n = size if size is not None else self.arraysize
107
+ rows = self._native.fetchmany(n)
108
+ return [tuple(r) for r in rows]
109
+
110
+ def fetchall(self) -> list[tuple[Any, ...]]:
111
+ """Fetch all remaining rows."""
112
+ self._check_open()
113
+ rows = self._native.fetchall()
114
+ return [tuple(r) for r in rows]
115
+
116
+ def fetch_arrow_table(self) -> Any:
117
+ """Fetch all remaining rows as a PyArrow Table.
118
+
119
+ mysql-connector-python has no native Arrow support, so this method
120
+ fetches all rows via ``fetchall()`` and converts them to a PyArrow
121
+ Table using column names from ``description``.
122
+ """
123
+ import pyarrow as pa
124
+
125
+ self._check_open()
126
+ rows = self._native.fetchall()
127
+ desc = self._native.description or []
128
+ col_names = [col[0] for col in desc]
129
+
130
+ if not rows:
131
+ # Return empty table with correct schema
132
+ arrays = [pa.array([], type=pa.utf8()) for _ in col_names]
133
+ return pa.table(dict(zip(col_names, arrays, strict=True)))
134
+
135
+ # Transpose rows to columns and let PyArrow infer types
136
+ columns: dict[str, list[Any]] = {name: [] for name in col_names}
137
+ for row in rows:
138
+ for i, name in enumerate(col_names):
139
+ columns[name].append(row[i])
140
+
141
+ return pa.table(columns)
142
+
143
+ def close(self) -> None:
144
+ """Close the cursor."""
145
+ if not self._closed:
146
+ self._native.close()
147
+ self._closed = True
148
+
149
+ def setinputsizes(self, sizes: Any) -> None:
150
+ """No-op — required by PEP 249."""
151
+
152
+ def setoutputsize(self, size: int, column: int | None = None) -> None:
153
+ """No-op — required by PEP 249."""
154
+
155
+ @property
156
+ def lastrowid(self) -> int | None:
157
+ """Return the last inserted row ID."""
158
+ return self._native.lastrowid # type: ignore[no-any-return]
159
+
160
+ def __enter__(self) -> Cursor:
161
+ return self
162
+
163
+ def __exit__(self, *exc: object) -> None:
164
+ self.close()
165
+
166
+ def __iter__(self) -> Cursor:
167
+ return self
168
+
169
+ def __next__(self) -> tuple[Any, ...]:
170
+ row = self.fetchone()
171
+ if row is None:
172
+ raise StopIteration
173
+ return row
ob_mysql/exceptions.py ADDED
@@ -0,0 +1,32 @@
1
+ """PEP 249 exceptions — re-exported from ob-driver-core for convenience.
2
+
3
+ Allows users to do: ``from ob_mysql.exceptions import ProgrammingError``
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ob_driver_core.exceptions import (
9
+ DatabaseError,
10
+ DataError,
11
+ Error,
12
+ IntegrityError,
13
+ InterfaceError,
14
+ InternalError,
15
+ NotSupportedError,
16
+ OperationalError,
17
+ ProgrammingError,
18
+ Warning,
19
+ )
20
+
21
+ __all__ = [
22
+ "Warning",
23
+ "Error",
24
+ "InterfaceError",
25
+ "DatabaseError",
26
+ "DataError",
27
+ "OperationalError",
28
+ "IntegrityError",
29
+ "InternalError",
30
+ "ProgrammingError",
31
+ "NotSupportedError",
32
+ ]
ob_mysql/type_codes.py ADDED
@@ -0,0 +1,47 @@
1
+ """PEP 249 type objects — re-exported from ob-driver-core plus MySQL field type mapping.
2
+
3
+ mysql-connector-python cursor.description returns integer field type constants.
4
+ The MYSQL_TYPE_MAP maps common field types to PEP 249 type objects.
5
+
6
+ MySQL field type constants:
7
+ https://dev.mysql.com/doc/dev/mysql-server/latest/field__types_8h.html
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ob_driver_core.type_codes import BINARY, DATETIME, NUMBER, ROWID, STRING
13
+
14
+ __all__ = ["STRING", "BINARY", "NUMBER", "DATETIME", "ROWID", "MYSQL_TYPE_MAP"]
15
+
16
+ # Common MySQL field type IDs from the wire protocol
17
+ MYSQL_TYPE_MAP: dict[int, object] = {
18
+ # Numeric types
19
+ 0: NUMBER, # DECIMAL
20
+ 1: NUMBER, # TINY (tinyint)
21
+ 2: NUMBER, # SHORT (smallint)
22
+ 3: NUMBER, # LONG (int)
23
+ 4: NUMBER, # FLOAT
24
+ 5: NUMBER, # DOUBLE
25
+ 8: NUMBER, # LONGLONG (bigint)
26
+ 9: NUMBER, # INT24 (mediumint)
27
+ 16: NUMBER, # BIT
28
+ 246: NUMBER, # NEWDECIMAL
29
+ # Date/time types
30
+ 7: DATETIME, # TIMESTAMP
31
+ 10: DATETIME, # DATE
32
+ 11: DATETIME, # TIME
33
+ 12: DATETIME, # DATETIME
34
+ 13: DATETIME, # YEAR
35
+ 14: DATETIME, # NEWDATE
36
+ # String types
37
+ 6: STRING, # NULL
38
+ 15: STRING, # VARCHAR
39
+ 245: STRING, # JSON
40
+ 253: STRING, # VAR_STRING
41
+ 254: STRING, # STRING
42
+ # Binary types
43
+ 249: BINARY, # TINY_BLOB
44
+ 250: BINARY, # MEDIUM_BLOB
45
+ 251: BINARY, # LONG_BLOB
46
+ 252: BINARY, # BLOB
47
+ }
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: ob-mysql
3
+ Version: 0.1.0
4
+ Summary: OrionBelt Semantic Layer driver for MySQL 8.0+ (DB-API 2.0)
5
+ Author-email: Ralf Becher <info@orionbelt.ai>
6
+ License: BSL-1.1
7
+ License-File: LICENSE
8
+ Keywords: db-api,mysql,obml,orionbelt,semantic-layer
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: mysql-connector-python>=8.0
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-mysql
24
+
25
+ OrionBelt Semantic Layer driver for MySQL 8.0+ (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-mysql
33
+ ```
34
+
35
+ ## License
36
+
37
+ BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,11 @@
1
+ ob_mysql/__init__.py,sha256=_DPqqdhMX6fEBUq2ZZogZKibAFK-pMP9TS65BF__U5Y,3489
2
+ ob_mysql/compiler.py,sha256=WTYZzdsJJiOxgIDS3NpDOpk_SG7_0weT4el-xuuBGsE,264
3
+ ob_mysql/connection.py,sha256=lIl18kMnYDXcGZcfC-y9nZFb8mBqaqqqJjYmqtU7308,1819
4
+ ob_mysql/cursor.py,sha256=Rx--AlIawRHLZNY1wtuB_GcgYZY8-Zp3vyAGxewSCy4,5813
5
+ ob_mysql/exceptions.py,sha256=fzp1a-Lavo-4uLoUb13AmV2G_kLpbvBTxWK2rox3cWQ,635
6
+ ob_mysql/type_codes.py,sha256=bzCvXJalRAvA3g01s_otmxmByLDxFo0OaK2KMgu45JE,1429
7
+ ob_mysql-0.1.0.dist-info/METADATA,sha256=0KPksEUdM76lIucYguNRff8SIhN_guiEFEqaVHLhIxU,1042
8
+ ob_mysql-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ ob_mysql-0.1.0.dist-info/entry_points.txt,sha256=gur83ZbIPcdNal493lSzBwZ3P6Qs3sHJnIB-VHH31tY,65
10
+ ob_mysql-0.1.0.dist-info/licenses/LICENSE,sha256=t-vr73N5s2w6yxpCnwo5r_QuB5_u6FuEovAYMjlfe7E,3378
11
+ ob_mysql-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [sqlalchemy.dialects]
2
+ ob.mysql = ob_mysql.dialect:OBMySQLDialect
@@ -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/.