refactor & add postgres
This commit is contained in:
parent
a7810606dd
commit
980f9ea791
38
README.md
38
README.md
@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
**Quicker** is a pythonic tool for querying databases.
|
**Quicker** is a pythonic tool for querying databases.
|
||||||
|
|
||||||
Quicker wraps popular Python packages:
|
Quicker wraps Python bindings on DBMS libraries:
|
||||||
|
|
||||||
- `mysqlclient` for MySQL.
|
- `mysqlclient` for MySQL.
|
||||||
- `psycopg2` for PostgreSQL (not implemented yet).
|
- `psycopg2` for PostgreSQL.
|
||||||
- Python builtin `sqlite` for SQLite (not implemented yet).
|
- `sqlite` from Python standard library for SQLite (not implemented yet).
|
||||||
|
|
||||||
Connection parameters will passed to "backend" module as is.
|
Connection parameters will passed to "backend" module as is.
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ pip install git+https://git.nxhs.cloud/ge/quicker
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
with Connection(**config) as db:
|
with Connection(**config) as db:
|
||||||
db.exec("sql query here...")
|
db.execute("sql query here...")
|
||||||
db.query("sql query here...") # query is alias for exec()
|
db.query("sql query here...") # query is alias for exec()
|
||||||
|
|
||||||
# Query is callable and you can also do this:
|
# Query is callable and you can also do this:
|
||||||
@ -38,9 +38,10 @@ with Connection(**config) as query:
|
|||||||
|
|
||||||
`Query` cannot be called itself, you must use `Connection` to correctly initialise `Query` object. Available methods and properties:
|
`Query` cannot be called itself, you must use `Connection` to correctly initialise `Query` object. Available methods and properties:
|
||||||
|
|
||||||
- `query()`, `exec()`. Execute SQL. There is You can use here this syntax: `query('SELECT * FROM users WHERE id = %s', (15,))`.
|
- `query()`, `execute()`. Execute SQL. There is You can use here this syntax: `query('SELECT * FROM users WHERE id = %s', (15,))`.
|
||||||
- `commit()`. Write changes into database.
|
- `commit()`. Write changes into database.
|
||||||
- `cursor`. Call [MySQLdb Cursor object](https://mysqlclient.readthedocs.io/user_guide.html#cursor-objects) methods directly.
|
- `cursor`. Access cursor object directly.
|
||||||
|
- `connection`. Access connection object directly.
|
||||||
|
|
||||||
Full example:
|
Full example:
|
||||||
|
|
||||||
@ -88,7 +89,30 @@ Changing database:
|
|||||||
from quicker import Connection
|
from quicker import Connection
|
||||||
|
|
||||||
with Connection(provider='mysql', read_default_file='~/.my.cnf') as db:
|
with Connection(provider='mysql', read_default_file='~/.my.cnf') as db:
|
||||||
db.query("INSERT INTO users VALUE (3, 'user2', 'user2@example.org')")
|
db.query("INSERT INTO `users` VALUE (3, 'user2', 'user2@example.org')")
|
||||||
```
|
```
|
||||||
|
|
||||||
Quicker by default make commit after closing context. Set option `commit=False` to disable automatic commit.
|
Quicker by default make commit after closing context. Set option `commit=False` to disable automatic commit.
|
||||||
|
|
||||||
|
For logging add following code:
|
||||||
|
|
||||||
|
```
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct access to Cursor object:
|
||||||
|
|
||||||
|
```
|
||||||
|
from quicker import Connection, make_list
|
||||||
|
|
||||||
|
# config declaration here...
|
||||||
|
|
||||||
|
with Connection(**config) as db:
|
||||||
|
db.cursor.execute('SELECT `id`, `name`, `email` FROM `users` WHERE `name` = %s', ('John',))
|
||||||
|
users = db.cursor.fetchall()
|
||||||
|
# Note: user is tuple! Convert it to list of dicts!
|
||||||
|
colnames = [desc[0] for desc in db.cursor.description]
|
||||||
|
users_list = make_list(colnames, users)
|
||||||
|
```
|
||||||
|
1
TODO.todo
Normal file
1
TODO.todo
Normal file
@ -0,0 +1 @@
|
|||||||
|
Query должен возвращать либо список диктов, либо None, сейчас None не возвращает
|
@ -2,7 +2,7 @@ version: '3.1'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
mysql:
|
||||||
image: mysql:8
|
image: mysql:8
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
restart: always
|
restart: always
|
||||||
@ -14,6 +14,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: myuser
|
||||||
|
POSTGRES_PASSWORD: example
|
||||||
|
POSTGRES_DB: mydb
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer
|
image: adminer
|
||||||
restart: always
|
restart: always
|
||||||
|
22
poetry.lock
generated
22
poetry.lock
generated
@ -1,23 +1,7 @@
|
|||||||
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
|
||||||
|
package = []
|
||||||
[[package]]
|
|
||||||
name = "mysqlclient"
|
|
||||||
version = "2.2.0"
|
|
||||||
description = "Python interface to MySQL"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "mysqlclient-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:68837b6bb23170acffb43ae411e47533a560b6360c06dac39aa55700972c93b2"},
|
|
||||||
{file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"},
|
|
||||||
{file = "mysqlclient-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:004fe1d30d2c2ff8072f8ea513bcec235fd9b896f70dad369461d0ad7e570e98"},
|
|
||||||
{file = "mysqlclient-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c6b142836c7dba4f723bf9c93cc46b6e5081d65b2af807f400dda9eb85a16d0"},
|
|
||||||
{file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"},
|
|
||||||
{file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"},
|
|
||||||
{file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.8"
|
||||||
content-hash = "462894cf2fdfd3121ca0c3a328dce7012ba1cecd25ba04d8a853c1fc21dde8a3"
|
content-hash = "935b488be9f11b23f14aa1ce3bed4013d88bd77462534b5e4fe5bb82b485bfe9"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "quicker"
|
name = "quicker"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Query databases quickly."
|
description = "Query databases quickly."
|
||||||
authors = ["ge <ge@nixhacks.net>"]
|
authors = ["ge <ge@nixhacks.net>"]
|
||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
@ -8,7 +8,6 @@ readme = "README.md"
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
mysqlclient = "^1.4.6"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
@ -1 +1,9 @@
|
|||||||
from .main import Connection
|
# ____ _ __
|
||||||
|
# / __ \__ __(_)____/ /_____ _____
|
||||||
|
# / / / / / / / / ___/ //_/ _ \/ ___/
|
||||||
|
# / /_/ / /_/ / / /__/ ,< / __/ /
|
||||||
|
# \___\_\__,_/_/\___/_/|_|\___/_/
|
||||||
|
#
|
||||||
|
# Quicker -- pythonic tool for querying databases
|
||||||
|
|
||||||
|
from .main import Connection, make_list
|
||||||
|
138
quicker/main.py
138
quicker/main.py
@ -1,5 +1,40 @@
|
|||||||
|
import logging
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Optional, List, Union, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _import(module_name: str, symbol: Optional[str] = None):
|
||||||
|
spec = importlib.util.find_spec(module_name)
|
||||||
|
if spec is None:
|
||||||
|
raise ImportError(f"Module '{module_name}' not found.")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
if symbol:
|
||||||
|
try:
|
||||||
|
return getattr(module, symbol)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImportError()
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def make_list(
|
||||||
|
column_names: List[str], rows: Union[tuple, Tuple[dict, ...]]
|
||||||
|
) -> List[dict]:
|
||||||
|
"""Convert output to list of dicts from tuples."""
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
if isinstance(row, dict):
|
||||||
|
data.append(row)
|
||||||
|
else:
|
||||||
|
item = {}
|
||||||
|
for i in range(len(row)):
|
||||||
|
item[column_names[i]] = row[i]
|
||||||
|
data.append(item)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class Provider(str, Enum):
|
class Provider(str, Enum):
|
||||||
@ -12,7 +47,12 @@ class Provider(str, Enum):
|
|||||||
class Connection:
|
class Connection:
|
||||||
"""
|
"""
|
||||||
Connection is context manager that allows to establish connection
|
Connection is context manager that allows to establish connection
|
||||||
with database.
|
with database and make queries. Example::
|
||||||
|
|
||||||
|
>>> from quicker import Connection
|
||||||
|
>>> with Connection(provider='mysql', read_default_file='~/.my.cnf') as q:
|
||||||
|
... server = q('SELECT * FROM `servers` WHERE `id` = %s', (1735781,))
|
||||||
|
>>>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@ -22,64 +62,90 @@ class Connection:
|
|||||||
):
|
):
|
||||||
if not provider:
|
if not provider:
|
||||||
raise ValueError('Database provider is not set')
|
raise ValueError('Database provider is not set')
|
||||||
self._provider = provider
|
self._provider = Provider(provider)
|
||||||
self._commit = commit
|
self._commit = commit
|
||||||
self._connection_args = kwargs
|
self._connection_args = kwargs
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
logger.debug(f'Database provider={self._provider}')
|
||||||
|
# -- MySQL / MariaDB --
|
||||||
if self._provider == Provider.MYSQL:
|
if self._provider == Provider.MYSQL:
|
||||||
MySQLdb = self._import('MySQLdb')
|
MySQLdb = _import('MySQLdb')
|
||||||
self._connection = MySQLdb.connect(**self._connection_args)
|
DictCursor = _import('MySQLdb.cursors', 'DictCursor')
|
||||||
|
try:
|
||||||
|
if self._connection_args['cursorclass']:
|
||||||
|
cursorclass = DictCursor
|
||||||
|
except KeyError:
|
||||||
|
cursorclass = DictCursor
|
||||||
|
self._connection = MySQLdb.connect(
|
||||||
|
**self._connection_args,
|
||||||
|
cursorclass=cursorclass,
|
||||||
|
)
|
||||||
|
logger.debug('Session started')
|
||||||
|
self._cursor = self._connection.cursor()
|
||||||
|
return Query(self)
|
||||||
|
# -- PostgreSQL --
|
||||||
|
if self._provider == Provider.POSTGRES:
|
||||||
|
psycopg2 = _import('psycopg2')
|
||||||
|
dbname = self._connection_args.pop('database')
|
||||||
|
self._connection_args['dbname'] = dbname
|
||||||
|
self._connection = psycopg2.connect(**self._connection_args)
|
||||||
self._cursor = self._connection.cursor()
|
self._cursor = self._connection.cursor()
|
||||||
return Query(self)
|
return Query(self)
|
||||||
|
|
||||||
def __exit__(self, exception_type, exception_value, exception_traceback):
|
def __exit__(self, exception_type, exception_value, exception_traceback):
|
||||||
if self._provider == Provider.MYSQL:
|
|
||||||
if self._commit:
|
if self._commit:
|
||||||
self._connection.commit()
|
self._connection.commit()
|
||||||
|
logger.debug('Changes commited into database')
|
||||||
self._cursor.close()
|
self._cursor.close()
|
||||||
self._connection.close()
|
self._connection.close()
|
||||||
|
logger.debug('Connection closed')
|
||||||
def _import(self, lib: str):
|
|
||||||
spec = importlib.util.find_spec(lib)
|
|
||||||
if spec is None:
|
|
||||||
raise ImportError(f"Module '{lib}' not found.")
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
class Query:
|
||||||
def __init__(self, connect):
|
def __init__(self, connect):
|
||||||
self._provider = connect._provider
|
self._provider = connect._provider
|
||||||
self._connection = connect._connection
|
self._connection = connect._connection
|
||||||
self.cursor = self._connection.cursor()
|
self._cursor = connect._cursor
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
return self.exec(*args, **kwargs)
|
return self.execute(*args, **kwargs)
|
||||||
|
|
||||||
def query(self, *args, **kwargs):
|
def query(self, *args, **kwargs):
|
||||||
return self.exec(*args, **kwargs)
|
return self.execute(*args, **kwargs)
|
||||||
|
|
||||||
def exec(self, *args, **kwargs):
|
def execute(self, *args, **kwargs) -> Union[List[dict], None]:
|
||||||
"""Execute SQL query and return output if available."""
|
"""Execute SQL query and return list of dicts or None."""
|
||||||
self.cursor.execute(*args, **kwargs)
|
|
||||||
self._fetchall = self.cursor.fetchall()
|
|
||||||
if self._fetchall is not None:
|
|
||||||
if self.cursor.description is not None:
|
|
||||||
self._field_names = [i[0] for i in self.cursor.description]
|
|
||||||
return self._conv(self._field_names, self._fetchall)
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
"""Commit changes into database."""
|
|
||||||
if self._provider == Provider.MYSQL:
|
if self._provider == Provider.MYSQL:
|
||||||
self._connection.commit()
|
self._cursor.execute(*args, **kwargs)
|
||||||
|
logger.debug(f'MySQLdb ran: {self._cursor._executed}')
|
||||||
|
self._fetchall = self._cursor.fetchall()
|
||||||
|
if self._provider == Provider.POSTGRES:
|
||||||
|
pgProgrammingError = _import('psycopg2', 'ProgrammingError')
|
||||||
|
self._cursor.execute(*args, **kwargs)
|
||||||
|
logger.debug(f'psycopg2 ran: {self._cursor.query}')
|
||||||
|
try:
|
||||||
|
self._fetchall = self._cursor.fetchall()
|
||||||
|
except pgProgrammingError as e:
|
||||||
|
self._fetchall = None
|
||||||
|
if self._fetchall is not None:
|
||||||
|
self._colnames = []
|
||||||
|
if self._cursor.description is not None:
|
||||||
|
self._colnames = [
|
||||||
|
desc[0] for desc in self._cursor.description
|
||||||
|
]
|
||||||
|
return make_list(self._colnames, self._fetchall)
|
||||||
|
return None
|
||||||
|
|
||||||
def _conv(self, field_names, rows) -> list[dict]:
|
def commit(self) -> None:
|
||||||
data = []
|
"""Commit changes into database."""
|
||||||
for row in rows:
|
self._connection.commit()
|
||||||
item = {}
|
logger.debug('Changes commited into database')
|
||||||
for i in range(len(row)):
|
|
||||||
item[field_names[i]] = row[i]
|
@property
|
||||||
data.append(item)
|
def connection(self):
|
||||||
return data
|
return self._connection
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cursor(self):
|
||||||
|
return self._cursor
|
||||||
|
Loading…
Reference in New Issue
Block a user