From 2ab95cbad525cfa6b8e3d930f64fbaa1667bf69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 11:49:39 +0100 Subject: [PATCH 1/7] Added SQL Server implementation using pyODBC --- setup.cfg | 2 + src/mayim/__init__.py | 4 ++ src/mayim/sql/sqlserver/__init__.py | 0 src/mayim/sql/sqlserver/executor.py | 55 +++++++++++++++++++++++ src/mayim/sql/sqlserver/interface.py | 66 ++++++++++++++++++++++++++++ src/mayim/sql/sqlserver/query.py | 25 +++++++++++ 6 files changed, 152 insertions(+) create mode 100644 src/mayim/sql/sqlserver/__init__.py create mode 100644 src/mayim/sql/sqlserver/executor.py create mode 100644 src/mayim/sql/sqlserver/interface.py create mode 100644 src/mayim/sql/sqlserver/query.py diff --git a/setup.cfg b/setup.cfg index be14904..dae4adc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,5 +42,7 @@ mysql = asyncmy sqlite = aiosqlite +sqlserver = + pyodbc [options.packages.find] where=src diff --git a/src/mayim/__init__.py b/src/mayim/__init__.py index b989dad..589fee5 100644 --- a/src/mayim/__init__.py +++ b/src/mayim/__init__.py @@ -10,6 +10,8 @@ from .sql.postgres.interface import PostgresPool from .sql.sqlite.executor import SQLiteExecutor from .sql.sqlite.interface import SQLitePool +from .sql.sqlserver.executor import SQLServerExecutor +from .sql.sqlserver.interface import SQLServerPool __version__ = version("mayim") @@ -24,6 +26,8 @@ "MysqlPool", "PostgresExecutor", "SQLiteExecutor", + "SQLServerExecutor", "PostgresPool", "SQLitePool", + "SQLServerPool", ) diff --git a/src/mayim/sql/sqlserver/__init__.py b/src/mayim/sql/sqlserver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mayim/sql/sqlserver/executor.py b/src/mayim/sql/sqlserver/executor.py new file mode 100644 index 0000000..353fadc --- /dev/null +++ b/src/mayim/sql/sqlserver/executor.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Any, Dict, Optional, Sequence + +from mayim.sql.sqlserver.query import SQLServerQuery +from ..executor import SQLExecutor + +try: + import pyodbc # noqa + + SQLSERVER_ENABLED = True +except ModuleNotFoundError: + SQLSERVER_ENABLED = False + + +class SQLServerExecutor(SQLExecutor): + """Executor for interfacing with a SQL Server database""" + + ENABLED = SQLSERVER_ENABLED + QUERY_CLASS = SQLServerQuery + POSITIONAL_SUB = r"?" + KEYWORD_SUB = r":\2" + + async def _run_sql( + self, + query: str, + name: str = "", + as_list: bool = False, + no_result: bool = False, + posargs: Optional[Sequence[Any]] = None, + params: Optional[Dict[str, Any]] = None, + ): + method_name = self._get_method(as_list=as_list) + async with self.pool.connection() as conn: + exec_values = list(posargs) if posargs else params + cursor = conn.execute(query, exec_values or []) + + columns = [column[0] for column in cursor.description] + if no_result: + return None + + raw = getattr(cursor, method_name)() + + if raw is None: + return None + + if not as_list: + return dict(zip(columns, raw)) + + results = [] + for row in raw: + print(row) + results.append(dict(zip(columns, row))) + + return results diff --git a/src/mayim/sql/sqlserver/interface.py b/src/mayim/sql/sqlserver/interface.py new file mode 100644 index 0000000..3c2a063 --- /dev/null +++ b/src/mayim/sql/sqlserver/interface.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import Optional +from urllib.parse import urlparse, parse_qs + +from mayim.base.interface import BaseInterface +from mayim.exception import MayimError + +try: + import pyodbc + + SQLSERVER_ENABLED = True +except ModuleNotFoundError: + SQLSERVER_ENABLED = False + + +class SQLServerPool(BaseInterface): + """Interface for connecting to a SQL Server database""" + + scheme = "mssql+pyodbc" + + def __init__(self, db_path: str): + self._db_path = db_path + super().__init__() + + def _setup_pool(self): + if not SQLSERVER_ENABLED: + raise MayimError( + "SQL Server driver not found. Try reinstalling Mayim: " + "pip install mayim[sqlserver]" + ) + + def _parse_url(self) -> str: + url = urlparse(self._db_path) + query = parse_qs(url.query) + driver = query.get("DRIVER", "") + conn_string = f"DRIVER={driver};SERVER={url.hostname};PORT={url.port};DATABASE={url.path.strip('/')};UID={url.username};PWD={url.password}" + return conn_string + + async def open(self): + """Open connections to the pool""" + conn_string = self._parse_url() + self._db = pyodbc.connect(conn_string) + + async def close(self): + """Close connections to the pool""" + self._db.close() + + @asynccontextmanager + async def connection(self, timeout: Optional[float] = None): + """Obtain a connection to the database + + Args: + timeout (float, optional): _Not implemented_. Defaults to `None`. + + Returns: + AsyncIterator[Connection]: Iterator that will yield a connection + + Yields: + Iterator[AsyncIterator[Connection]]: A database connection + """ + if not self._db: + await self.open() + + yield self._db diff --git a/src/mayim/sql/sqlserver/query.py b/src/mayim/sql/sqlserver/query.py new file mode 100644 index 0000000..61f0a76 --- /dev/null +++ b/src/mayim/sql/sqlserver/query.py @@ -0,0 +1,25 @@ +import re + +from mayim.exception import MayimError +from mayim.sql.query import ParamType, SQLQuery + + +class SQLServerQuery(SQLQuery): + __slots__ = ("name", "text", "param_type") + PATTERN_POSITIONAL_PARAMETER = re.compile(r"\?") + PATTERN_KEYWORD_PARAMETER = re.compile(r"\:[a-z_][a-z0-9_]") + + def __init__(self, name: str, text: str) -> None: + super().__init__(name, text) + positional_argument_exists = bool( + self.PATTERN_POSITIONAL_PARAMETER.search(self.text) + ) + keyword_argument_exists = bool( + self.PATTERN_KEYWORD_PARAMETER.search(self.text) + ) + if keyword_argument_exists: + raise MayimError("Only Positional arguments allowed in pyODBC") + if positional_argument_exists: + self.param_type = ParamType.POSITIONAL + else: + self.param_type = ParamType.NONE From 6b92ff7c912627e26a39c7e68437944c7c5a55f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:01:48 +0100 Subject: [PATCH 2/7] Added sql server install note to docs --- docs/src/guide/install.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/src/guide/install.md b/docs/src/guide/install.md index e874d4c..663e39e 100755 --- a/docs/src/guide/install.md +++ b/docs/src/guide/install.md @@ -8,11 +8,13 @@ You can install Mayim using PIP: pip install mayim ``` -To get access to support for a specific data source, make sure you install the appropriate dependencies. You must install one of the following drivers. +To get access to support for a specific data source, make sure you install the appropriate dependencies. You must +install one of the following drivers. ## Postgres Dependencies: + - [psycopg3](https://www.psycopg.org/psycopg3/) Either install it independently: @@ -30,6 +32,7 @@ pip install mayim[postgres] ## MySQL Dependencies: + - [asyncmy](https://github.com/long2ice/asyncmy) Either install it independently: @@ -47,6 +50,7 @@ pip install mayim[mysql] ## SQLite Dependencies: + - [aiosqlite](https://github.com/omnilib/aiosqlite) Either install it independently: @@ -60,3 +64,21 @@ Or, as a convenience: ``` pip install mayim[sqlite] ``` + +## SQL Server + +Dependencies: + +- [aiosqlite](https://github.com/mkleehammer/pyodbc) + +Either install it independently: + +``` +pip install pyodbc +``` + +Or, as a convenience: + +``` +pip install mayim[sqlserver] +``` From b8c874bc80ac90058681266e64792926b4813ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:02:03 +0100 Subject: [PATCH 3/7] Removed print statement --- src/mayim/sql/sqlserver/executor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mayim/sql/sqlserver/executor.py b/src/mayim/sql/sqlserver/executor.py index 353fadc..b6cf23a 100644 --- a/src/mayim/sql/sqlserver/executor.py +++ b/src/mayim/sql/sqlserver/executor.py @@ -49,7 +49,6 @@ async def _run_sql( results = [] for row in raw: - print(row) results.append(dict(zip(columns, row))) return results From 2936ee301acc72b41d9722ad5b9e93f02d8955f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:07:46 +0100 Subject: [PATCH 4/7] Removed line breaks pycharm automatically added to install docs --- docs/src/guide/install.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/src/guide/install.md b/docs/src/guide/install.md index 663e39e..7109659 100755 --- a/docs/src/guide/install.md +++ b/docs/src/guide/install.md @@ -8,13 +8,11 @@ You can install Mayim using PIP: pip install mayim ``` -To get access to support for a specific data source, make sure you install the appropriate dependencies. You must -install one of the following drivers. +To get access to support for a specific data source, make sure you install the appropriate dependencies. You must install one of the following drivers. ## Postgres Dependencies: - - [psycopg3](https://www.psycopg.org/psycopg3/) Either install it independently: @@ -32,7 +30,6 @@ pip install mayim[postgres] ## MySQL Dependencies: - - [asyncmy](https://github.com/long2ice/asyncmy) Either install it independently: @@ -50,7 +47,6 @@ pip install mayim[mysql] ## SQLite Dependencies: - - [aiosqlite](https://github.com/omnilib/aiosqlite) Either install it independently: @@ -68,8 +64,7 @@ pip install mayim[sqlite] ## SQL Server Dependencies: - -- [aiosqlite](https://github.com/mkleehammer/pyodbc) +- [pyodbc](https://github.com/mkleehammer/pyodbc) Either install it independently: From 8e5ba5190a90e0cb1feada6a0f39bfa5ed7973a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:11:23 +0100 Subject: [PATCH 5/7] Removed unnecessary check for none --- src/mayim/sql/sqlserver/executor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/mayim/sql/sqlserver/executor.py b/src/mayim/sql/sqlserver/executor.py index b6cf23a..f8355a5 100644 --- a/src/mayim/sql/sqlserver/executor.py +++ b/src/mayim/sql/sqlserver/executor.py @@ -41,9 +41,6 @@ async def _run_sql( raw = getattr(cursor, method_name)() - if raw is None: - return None - if not as_list: return dict(zip(columns, raw)) From c021e01d2435c5f29d46ca0c2f93285acedb8acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:25:27 +0100 Subject: [PATCH 6/7] Fixed code style using make pretty --- src/mayim/sql/sqlserver/executor.py | 15 ++++++++------- src/mayim/sql/sqlserver/interface.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/mayim/sql/sqlserver/executor.py b/src/mayim/sql/sqlserver/executor.py index f8355a5..38d8ac4 100644 --- a/src/mayim/sql/sqlserver/executor.py +++ b/src/mayim/sql/sqlserver/executor.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Sequence from mayim.sql.sqlserver.query import SQLServerQuery + from ..executor import SQLExecutor try: @@ -22,13 +23,13 @@ class SQLServerExecutor(SQLExecutor): KEYWORD_SUB = r":\2" async def _run_sql( - self, - query: str, - name: str = "", - as_list: bool = False, - no_result: bool = False, - posargs: Optional[Sequence[Any]] = None, - params: Optional[Dict[str, Any]] = None, + self, + query: str, + name: str = "", + as_list: bool = False, + no_result: bool = False, + posargs: Optional[Sequence[Any]] = None, + params: Optional[Dict[str, Any]] = None, ): method_name = self._get_method(as_list=as_list) async with self.pool.connection() as conn: diff --git a/src/mayim/sql/sqlserver/interface.py b/src/mayim/sql/sqlserver/interface.py index 3c2a063..c4e22fa 100644 --- a/src/mayim/sql/sqlserver/interface.py +++ b/src/mayim/sql/sqlserver/interface.py @@ -2,7 +2,7 @@ from contextlib import asynccontextmanager from typing import Optional -from urllib.parse import urlparse, parse_qs +from urllib.parse import parse_qs, urlparse from mayim.base.interface import BaseInterface from mayim.exception import MayimError From bc3f8fda5b4489cca4adfdc71b26b8a09db740d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Hedtst=C3=BCck?= Date: Thu, 17 Nov 2022 12:32:21 +0100 Subject: [PATCH 7/7] Fixed too long line --- src/mayim/sql/sqlserver/interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mayim/sql/sqlserver/interface.py b/src/mayim/sql/sqlserver/interface.py index c4e22fa..f8d3eb5 100644 --- a/src/mayim/sql/sqlserver/interface.py +++ b/src/mayim/sql/sqlserver/interface.py @@ -35,7 +35,11 @@ def _parse_url(self) -> str: url = urlparse(self._db_path) query = parse_qs(url.query) driver = query.get("DRIVER", "") - conn_string = f"DRIVER={driver};SERVER={url.hostname};PORT={url.port};DATABASE={url.path.strip('/')};UID={url.username};PWD={url.password}" + conn_string = ( + f"DRIVER={driver};SERVER={url.hostname};PORT={url.port};" + f"DATABASE={url.path.strip('/')};" + f"UID={url.username};PWD={url.password};" + ) return conn_string async def open(self):