Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
3df72090f9 | |||
ff69b68ba3 | |||
a511550f71 | |||
9ac7c48805 | |||
0a24ded7ec |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
__pychache__/
|
__pychache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
dist/
|
dist/
|
||||||
|
db.sqlite3
|
||||||
|
*.db
|
||||||
|
24
COPYING
Normal file
24
COPYING
Normal 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/>
|
136
README.md
136
README.md
@ -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 Python bindings on DBMS libraries:
|
Quicker wraps Python libraries:
|
||||||
|
|
||||||
- `mysqlclient` for MySQL.
|
- `mysqlclient` for MySQL.
|
||||||
- `psycopg2` for PostgreSQL.
|
- `psycopg2` for PostgreSQL.
|
||||||
- `sqlite` from Python standard library 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,30 +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.execute("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()`, `execute()`. 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`. Access cursor object directly.
|
- `cursor`. Access cursor object directly.
|
||||||
- `connection`. Access connection 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
|
||||||
|
|
||||||
|
|
||||||
@ -61,58 +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.
|
|
||||||
|
|
||||||
For logging add following code:
|
## Logging
|
||||||
|
|
||||||
```
|
For logging add following code in your module:
|
||||||
|
|
||||||
|
```python
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
```
|
```
|
||||||
|
|
||||||
Direct access to Cursor object:
|
## Direct access to Cursor object
|
||||||
|
|
||||||
```
|
```python
|
||||||
from quicker import Connection, make_list
|
from quicker import Connection, mklist
|
||||||
|
|
||||||
# config declaration here...
|
# config declaration here...
|
||||||
|
|
||||||
with Connection(**config) as db:
|
with Connection(**config) as db:
|
||||||
db.cursor.execute('SELECT `id`, `name`, `email` FROM `users` WHERE `name` = %s', ('John',))
|
db.cursor.execute('SELECT id, name, email FROM users WHERE name = %s', ('John',))
|
||||||
users = db.cursor.fetchall()
|
users = db.cursor.fetchall()
|
||||||
# Note: user is tuple! Convert it to list of dicts!
|
# Note: "users" is tuple! Convert it to list of dicts!
|
||||||
colnames = [desc[0] for desc in db.cursor.description]
|
colnames = [desc[0] for desc in db.cursor.description]
|
||||||
users_list = make_list(colnames, users)
|
users_list = mklist(colnames, users)
|
||||||
```
|
```
|
||||||
|
56
logo.svg
Normal file
56
logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 107 KiB |
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "quicker"
|
name = "quicker"
|
||||||
version = "0.2.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"
|
||||||
|
@ -1,3 +1,15 @@
|
|||||||
|
#
|
||||||
|
# .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 logging
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@ -21,10 +33,12 @@ def _import(module_name: str, symbol: Optional[str] = None):
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
def make_list(
|
def mklist(column_names: List[str], rows: Union[tuple, Tuple[dict, ...]]) -> List[dict]:
|
||||||
column_names: List[str], rows: Union[tuple, Tuple[dict, ...]]
|
"""
|
||||||
) -> List[dict]:
|
Convert output to list of dicts from tuples. `rows` can be
|
||||||
"""Convert output to list of dicts from tuples."""
|
default tuple or tuple of dicts if MySQL provider is used with
|
||||||
|
MySQLdb.cursors.DictCursor cursor class.
|
||||||
|
"""
|
||||||
data = []
|
data = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if isinstance(row, dict):
|
if isinstance(row, dict):
|
||||||
@ -38,7 +52,6 @@ def make_list(
|
|||||||
|
|
||||||
|
|
||||||
class Provider(str, Enum):
|
class Provider(str, Enum):
|
||||||
|
|
||||||
MYSQL = 'mysql'
|
MYSQL = 'mysql'
|
||||||
POSTGRES = 'postgres'
|
POSTGRES = 'postgres'
|
||||||
SQLITE = 'sqlite'
|
SQLITE = 'sqlite'
|
||||||
@ -55,26 +68,17 @@ class Connection:
|
|||||||
>>>
|
>>>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, provider: Provider, commit: bool = True, **kwargs):
|
||||||
provider: Provider = None,
|
|
||||||
commit: bool = True,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
if not provider:
|
|
||||||
raise ValueError('Database provider is not set')
|
|
||||||
self._provider = Provider(provider)
|
self._provider = Provider(provider)
|
||||||
self._commit = commit
|
self._commit = commit
|
||||||
self._connection_args = kwargs
|
self._connection_args = kwargs
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
logger.debug(f'Database provider={self._provider}')
|
logger.debug(f'Database provider={self._provider}')
|
||||||
# -- MySQL / MariaDB --
|
|
||||||
if self._provider == Provider.MYSQL:
|
if self._provider == Provider.MYSQL:
|
||||||
MySQLdb = _import('MySQLdb')
|
MySQLdb = _import('MySQLdb')
|
||||||
DictCursor = _import('MySQLdb.cursors', 'DictCursor')
|
DictCursor = _import('MySQLdb.cursors', 'DictCursor')
|
||||||
try:
|
try:
|
||||||
if self._connection_args['cursorclass']:
|
cursorclass = self._connection_args.pop('cursorclass')
|
||||||
cursorclass = DictCursor
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
cursorclass = DictCursor
|
cursorclass = DictCursor
|
||||||
self._connection = MySQLdb.connect(
|
self._connection = MySQLdb.connect(
|
||||||
@ -83,23 +87,35 @@ class Connection:
|
|||||||
)
|
)
|
||||||
logger.debug('Session started')
|
logger.debug('Session started')
|
||||||
self._cursor = self._connection.cursor()
|
self._cursor = self._connection.cursor()
|
||||||
return Query(self)
|
self._queryobj = Query(self)
|
||||||
# -- PostgreSQL --
|
|
||||||
if self._provider == Provider.POSTGRES:
|
if self._provider == Provider.POSTGRES:
|
||||||
psycopg2 = _import('psycopg2')
|
psycopg2 = _import('psycopg2')
|
||||||
dbname = self._connection_args.pop('database')
|
dbname = self._connection_args.pop('database')
|
||||||
self._connection_args['dbname'] = dbname
|
self._connection_args['dbname'] = dbname
|
||||||
self._connection = psycopg2.connect(**self._connection_args)
|
self._connection = psycopg2.connect(**self._connection_args)
|
||||||
self._cursor = self._connection.cursor()
|
self._cursor = self._connection.cursor()
|
||||||
return Query(self)
|
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):
|
def __exit__(self, exception_type, exception_value, exception_traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
if self._commit:
|
if self._commit:
|
||||||
|
logger.debug('Commiting changes into database')
|
||||||
self._connection.commit()
|
self._connection.commit()
|
||||||
logger.debug('Changes commited into database')
|
logger.debug('Closing cursor and connection')
|
||||||
self._cursor.close()
|
self._cursor.close()
|
||||||
self._connection.close()
|
self._connection.close()
|
||||||
logger.debug('Connection closed')
|
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
class Query:
|
||||||
@ -128,13 +144,17 @@ class Query:
|
|||||||
self._fetchall = self._cursor.fetchall()
|
self._fetchall = self._cursor.fetchall()
|
||||||
except pgProgrammingError as e:
|
except pgProgrammingError as e:
|
||||||
self._fetchall = None
|
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:
|
if self._fetchall is not None:
|
||||||
self._colnames = []
|
self._colnames = []
|
||||||
if self._cursor.description is not None:
|
if self._cursor.description is not None:
|
||||||
self._colnames = [
|
self._colnames = [
|
||||||
desc[0] for desc in self._cursor.description
|
desc[0] for desc in self._cursor.description
|
||||||
]
|
]
|
||||||
return make_list(self._colnames, self._fetchall)
|
return mklist(self._colnames, self._fetchall)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def commit(self) -> None:
|
def commit(self) -> None:
|
@ -1,9 +0,0 @@
|
|||||||
# ____ _ __
|
|
||||||
# / __ \__ __(_)____/ /_____ _____
|
|
||||||
# / / / / / / / / ___/ //_/ _ \/ ___/
|
|
||||||
# / /_/ / /_/ / / /__/ ,< / __/ /
|
|
||||||
# \___\_\__,_/_/\___/_/|_|\___/_/
|
|
||||||
#
|
|
||||||
# Quicker -- pythonic tool for querying databases
|
|
||||||
|
|
||||||
from .main import Connection, make_list
|
|
Loading…
x
Reference in New Issue
Block a user