Compare commits

...

7 Commits

Author SHA1 Message Date
ge
3df72090f9 add __all__ 2023-07-25 01:54:14 +03:00
ge
ff69b68ba3 upd, add sqlite 2023-07-25 01:40:12 +03:00
ge
a511550f71 add license 2023-07-19 09:48:37 +03:00
ge
9ac7c48805 Upload files to "/" 2023-07-19 09:46:35 +03:00
ge
0a24ded7ec Update README.md 2023-07-19 09:46:12 +03:00
ge
40f121ad84 upd 2023-07-19 06:45:26 +03:00
ge
980f9ea791 refactor & add postgres 2023-07-19 06:44:58 +03:00
10 changed files with 370 additions and 156 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
__pychache__/ __pychache__/
*.pyc *.pyc
dist/ dist/
db.sqlite3
*.db

24
COPYING Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

148
README.md
View File

@ -1,20 +1,20 @@
``` <img width="320px" src="logo.svg"/>
____ _ __
/ __ \__ __(_)____/ /_____ _____
/ / / / / / / / ___/ //_/ _ \/ ___/
/ /_/ / /_/ / / /__/ ,< / __/ /
\___\_\__,_/_/\___/_/|_|\___/_/
```
**Quicker** is a pythonic tool for querying databases. **Quicker** is a pythonic 🐍 tool for querying databases.
Quicker wraps popular Python packages: Quicker wraps Python libraries:
- `mysqlclient` for MySQL. - `mysqlclient` for MySQL.
- `psycopg2` for PostgreSQL (not implemented yet). - `psycopg2` for PostgreSQL.
- Python builtin `sqlite` for SQLite (not implemented yet). - `sqlite3` from Python standard library for SQLite.
Connection parameters will passed to "backend" module as is. # Why is it needed?
At work, I periodically have to make queries to different databases and then somehow process the information received. This may be necessary for one-time use, so I don't want to write a lot of code. You may also want to do all the work right in the interactive Python shell.
Quicker interface is as simple as possible, thanks to which lazy system administrators can now effortlessly extract data from the database.
Of course, this library **should not be used in production**. This is just a small assistant in routine tasks.
# Installation # Installation
@ -24,29 +24,32 @@ pip install git+https://git.nxhs.cloud/ge/quicker
# Usage # Usage
`Connection` is context manages and must be used with `with` keyword. `Connection` returns `Query` callable object. `Query` can be called in this ways: Quicker uses a context manager. All that is needed for work is to pass connection parameters to object and write the queries themselves. See MySQL example:
```python ```python
with Connection(**config) as db: from quicker import Connection
db.exec("sql query here...")
db.query("sql query here...") # query is alias for exec()
# Query is callable and you can also do this:
with Connection(**config) as query: with Connection(provider='mysql', read_default_file='~/.my.cnf') as query:
query("sql query here...") users = query("SELECT * FROM `users` WHERE admin = 'N'")
``` ```
`Query` cannot be called itself, you must use `Connection` to correctly initialise `Query` object. Available methods and properties: `Connection` object initialises `Query` callable object for interacting with cursor. You can use `query("sql there..")` or `query.execute("sql...")` syntax. There are the same.
- `query()`, `exec()`. Execute SQL. There is You can use here this syntax: `query('SELECT * FROM users WHERE id = %s', (15,))`. `Query` object methods and properties:
- `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: You don't need to commit to the database, Quicker will do it for you, but if you need to, you can commit manually calling `query.commit()`. You can also turn off automatic commit when creating a `Connection` object — pass it the argument `commit=Fasle`.
That's not all — Quicker converts the received data into a list of dictionaries. The list will be empty if nothing was found for the query. If the request does not imply a response, `None` will be returned.
## MySQL example
```python ```python
import json
from quicker import Connection from quicker import Connection
@ -60,35 +63,86 @@ config = {
} }
with Connection(**config) as query: with Connection(**config) as query:
users = query("SELECT * FROM `users`") query(
"""
CREATE TABLE IF NOT EXISTS users (
id int AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
)
query(
"INSERT INTO users VALUES (NULL, %s, %s, current_timestamp)",
('john', 'john@exmpl.org',)
)
query.commit()
users = query("SELECT * FROM users")
print(json.dumps(users, indent=4)) print('ID\t NAME\t EMAIL')
for user in users:
print(user['id'], '\t', user['name'], '\t', user['email'])
``` ```
`users` will presented as list of dicts: ## PostgreSQL example
```python ```python
[ import logging
{
'id': 1,
'name': 'test',
'email': 'noreply@localhost'
},
{
'id': 2,
'name': 'user1',
'email': 'my@example.com'
}
]
```
Changing database:
```python
from quicker import Connection from quicker import Connection
with Connection(provider='mysql', read_default_file='~/.my.cnf') as db:
db.query("INSERT INTO users VALUE (3, 'user2', 'user2@example.org')") logging.basicConfig(level=logging.DEBUG)
config = {
'provider': 'postgres',
'host': '127.0.0.1',
'port': 5432,
'user': 'myuser',
'database': 'mydb',
'password': 'example',
}
with Connection(**config) as query:
query(
"""
CREATE TABLE IF NOT EXISTS users (
id serial PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
)
query(
"INSERT INTO users VALUES ((SELECT MAX(id)+1 FROM users), %s, %s, current_timestamp)",
('phil', 'phil@exmpl.org',)
)
``` ```
Quicker by default make commit after closing context. Set option `commit=False` to disable automatic commit.
## Logging
For logging add following code in your module:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
```
## Direct access to Cursor object
```python
from quicker import Connection, mklist
# 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: "users" is tuple! Convert it to list of dicts!
colnames = [desc[0] for desc in db.cursor.description]
users_list = mklist(colnames, users)
```

View File

@ -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

56
logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 107 KiB

22
poetry.lock generated
View File

@ -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"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "quicker" name = "quicker"
version = "0.1.0" version = "0.3.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"]

171
quicker.py Normal file
View File

@ -0,0 +1,171 @@
#
# .88888. oo dP
# d8' `8b 88
# 88 88 dP dP dP .d8888b. 88 .dP .d8888b. 88d888b.
# 88 db 88 88 88 88 88' `"" 88888" 88ooood8 88' `88
# Y8. Y88P 88. .88 88 88. ... 88 `8b. 88. ... 88
# `8888PY8b `88888P' dP `88888P' dP `YP `88888P' dP
#
# Quicker -- pythonic tool for querying databases.
__all__ = ['Connection', 'mklist', 'Provider']
import logging
import importlib.util
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 mklist(column_names: List[str], rows: Union[tuple, Tuple[dict, ...]]) -> List[dict]:
"""
Convert output to list of dicts from tuples. `rows` can be
default tuple or tuple of dicts if MySQL provider is used with
MySQLdb.cursors.DictCursor cursor class.
"""
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):
MYSQL = 'mysql'
POSTGRES = 'postgres'
SQLITE = 'sqlite'
class Connection:
"""
Connection is context manager that allows to establish connection
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, provider: Provider, commit: bool = True, **kwargs):
self._provider = Provider(provider)
self._commit = commit
self._connection_args = kwargs
logger.debug(f'Database provider={self._provider}')
if self._provider == Provider.MYSQL:
MySQLdb = _import('MySQLdb')
DictCursor = _import('MySQLdb.cursors', 'DictCursor')
try:
cursorclass = self._connection_args.pop('cursorclass')
except KeyError:
cursorclass = DictCursor
self._connection = MySQLdb.connect(
**self._connection_args,
cursorclass=cursorclass,
)
logger.debug('Session started')
self._cursor = self._connection.cursor()
self._queryobj = Query(self)
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._queryobj = Query(self)
if self._provider == Provider.SQLITE:
sqlite3 = _import('sqlite3') # Python may built without SQLite
self._connection = sqlite3.connect(**self._connection_args)
self._cursor = self._connection.cursor()
self._queryobj = Query(self)
def __enter__(self):
return self._queryobj
def __exit__(self, exception_type, exception_value, exception_traceback):
self.close()
def close(self):
if self._commit:
logger.debug('Commiting changes into database')
self._connection.commit()
logger.debug('Closing cursor and connection')
self._cursor.close()
self._connection.close()
class Query:
def __init__(self, connect):
self._provider = connect._provider
self._connection = connect._connection
self._cursor = connect._cursor
def __call__(self, *args, **kwargs):
return self.execute(*args, **kwargs)
def query(self, *args, **kwargs):
return self.execute(*args, **kwargs)
def execute(self, *args, **kwargs) -> Union[List[dict], None]:
"""Execute SQL query and return list of dicts or None."""
if self._provider == Provider.MYSQL:
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._provider == Provider.SQLITE:
self._cursor.execute(*args, **kwargs)
logger.debug(f'sqlite3 ran: {args}')
self._fetchall = self._cursor.fetchall()
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 mklist(self._colnames, self._fetchall)
return None
def commit(self) -> None:
"""Commit changes into database."""
self._connection.commit()
logger.debug('Changes commited into database')
@property
def connection(self):
return self._connection
@property
def cursor(self):
return self._cursor

View File

@ -1 +0,0 @@
from .main import Connection

View File

@ -1,85 +0,0 @@
import importlib.util
from enum import Enum
class Provider(str, Enum):
MYSQL = 'mysql'
POSTGRES = 'postgres'
SQLITE = 'sqlite'
class Connection:
"""
Connection is context manager that allows to establish connection
with database.
"""
def __init__(self,
provider: Provider = None,
commit: bool = True,
**kwargs
):
if not provider:
raise ValueError('Database provider is not set')
self._provider = provider
self._commit = commit
self._connection_args = kwargs
def __enter__(self):
if self._provider == Provider.MYSQL:
MySQLdb = self._import('MySQLdb')
self._connection = MySQLdb.connect(**self._connection_args)
self._cursor = self._connection.cursor()
return Query(self)
def __exit__(self, exception_type, exception_value, exception_traceback):
if self._provider == Provider.MYSQL:
if self._commit:
self._connection.commit()
self._cursor.close()
self._connection.close()
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:
def __init__(self, connect):
self._provider = connect._provider
self._connection = connect._connection
self.cursor = self._connection.cursor()
def __call__(self, *args, **kwargs):
return self.exec(*args, **kwargs)
def query(self, *args, **kwargs):
return self.exec(*args, **kwargs)
def exec(self, *args, **kwargs):
"""Execute SQL query and return output if available."""
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:
self._connection.commit()
def _conv(self, field_names, rows) -> list[dict]:
data = []
for row in rows:
item = {}
for i in range(len(row)):
item[field_names[i]] = row[i]
data.append(item)
return data