Skip to content

Commit 5b1b6eb

Browse files
committed
Implemented basic functionality for DatabaseManager; Updated docker and docker-compose file; Added tests and system tests; Updated README; Updated requirements.txt
1 parent 4985b81 commit 5b1b6eb

12 files changed

+216
-106
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea
22
.env
33
*.pyc
4+
.coverage

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
language: python
2+
python:
3+
- 2.7
4+
install:
5+
- pip install -r requirements.txt
6+
- pip install coveralls
7+
script:
8+
- make test_ci
9+
after_success:
10+
coveralls

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pypi_upload:
2424

2525
.PHONY: test
2626
test: env
27-
@$(NOSE) $(CODE_DIR)/tests.py
27+
@$(NOSE) $(CODE_DIR)/tests
2828

2929
.PHONY: system_test
3030
system_test: env
@@ -35,7 +35,7 @@ system_test: env
3535
.PHONY: test_ci
3636
test_ci: env
3737
# target: test_ci - Run tests command adapt for CI systems
38-
@$(NOSE) $(CODE_DIR)/tests.py
38+
@$(NOSE) $(CODE_DIR)/tests
3939

4040
.PHONY: test_coverage
4141
# target: test_coverage - Run tests with coverage

README.md

+81-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,75 @@ pgclient
33

44
pgclient - yet another pool-based psycopg2 wrapper.
55

6+
The client is fully based on thread-safe connections pool and safe transactions executing
7+
68

79
Quick start
810
===========
9-
10-
11-
11+
Main class to operate with database is a `PostgresClient`
12+
13+
14+
Initialization
15+
--------------
16+
17+
from pgclient.client import PostgresClient
18+
19+
20+
pg_client = PostgresClient(dsn='{dsn_string}')
21+
# OR
22+
pg_client = PostgresClient(username='test', password='test', ...)
23+
24+
Database raw request
25+
--------------------
26+
27+
Assume that test data schema is the following:
28+
29+
* TABLE: users
30+
* SCHEMA: name: VARCHAR, id: INTEGER
31+
32+
**Basic cursor**
33+
34+
Result set index based access
35+
36+
with self.pg_client.cursor as cursor:
37+
cursor.execute('SELECT * FROM users')
38+
39+
users = cursor.fetchall()
40+
username = users[0][0] # (OR users[0][1])
41+
42+
43+
**Dict cursor**
44+
45+
with self.pg_client.dict_cursor as cursor:
46+
cursor.execute('SELECT * FROM users')
47+
48+
users = cursor.fetchall()
49+
user = users[0]
50+
print(user['name'])
51+
52+
53+
**Named-tuple cursor**
54+
55+
with self.pg_client.nt_cursor as cursor:
56+
cursor.execute('SELECT * FROM users')
57+
result = cursor.fetchall()
58+
59+
user = users[0]
60+
print(user.name)
61+
62+
63+
Safe transactions
64+
-----------------
65+
66+
All requests inside `with` context will be executed and automatically committed within one transaction
67+
68+
with self.pg_client.cursor as transaction:
69+
transaction.execute('INSERT INTO users VALUES name="Mark"')
70+
transaction.execute('INSERT INTO users VALUES name="Paolo"')
71+
transaction.execute('SELECT * FROM users')
72+
73+
users = transaction.fetchall()
74+
1275

1376
Installation
1477
============
@@ -26,6 +89,21 @@ Install package
2689
pip install pgclient
2790

2891

92+
System test
93+
===========
94+
To run integration test you need to install the following:
95+
96+
* [Docker](https://www.docker.com/)
97+
* [Docker compose](https://docs.docker.com/compose/)
98+
99+
100+
**Run system tests:**
101+
102+
* Run postgresql container: `docker-compose up -d postgresql`
103+
* Run system tests: `make system_test`
104+
* Stop postgresql container: `docker-compose stop postgresql`
105+
106+
29107
Bug tracker
30108
===========
31109

docker-compose.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
system_test:
2-
build: system_test
1+
postgresql:
2+
build: pgclient/system_test
33
ports:
4-
- "5432:5432"
4+
- "0.0.0.0:5432:5432"
55
environment:
66
POSTGRES_PASSWORD: test
7+
PSQL_ENTRYPOINT: /docker-entrypoint-initdb.d

pgclient/client.py

+23-42
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
# -*- coding: utf-8 -*-
2+
from contextlib import contextmanager
3+
24
import psycopg2
35
import psycopg2.pool as pgpool
46
import psycopg2.extras as pg_extras
5-
import abc
6-
import six
7-
from contextlib import contextmanager
8-
97

10-
# TODO: use connection pool by default
11-
# TODO: add transactional contextmanager
128

13-
14-
class DatabaseManager(object):
9+
class PostgresClient(object):
1510
def __init__(self, dsn=None, database=None, user=None, password=None,
1611
host=None, port=None, pool_size=1):
1712
self.dsn = dsn
@@ -23,7 +18,7 @@ def __init__(self, dsn=None, database=None, user=None, password=None,
2318
raise ValueError('Wrong pool_size value. Must be >= 1. '
2419
'Current: {}'.format(pool_size))
2520
# Init thread-safe connection pool
26-
self.pool = pgpool.ThreadedConnectionPool(
21+
self._pool = pgpool.ThreadedConnectionPool(
2722
minconn=1, maxconn=pool_size, **conn_params)
2823

2924
@property
@@ -32,7 +27,7 @@ def connection(self):
3227
3328
:return: postgresql connection instance
3429
"""
35-
return self.pool.getconn()
30+
return self._pool.getconn()
3631

3732
@contextmanager
3833
def _get_cursor(self, cursor_factory=None):
@@ -42,48 +37,34 @@ def _get_cursor(self, cursor_factory=None):
4237
conn.commit()
4338
except psycopg2.DatabaseError as err:
4439
conn.rollback()
45-
raise psycopg2.DatabaseError(err)
40+
raise psycopg2.DatabaseError(err.message)
4641
finally:
47-
self.pool.putconn(conn)
42+
self._pool.putconn(conn)
4843

49-
# TODO: rename it
5044
@property
5145
def cursor(self):
46+
"""Default index based cursor"""
47+
5248
return self._get_cursor()
5349

5450
@property
5551
def dict_cursor(self):
56-
"""Return dict cursor. It enables accessing via column names instead
57-
of indexes
52+
"""Return dict cursor. It enables to get fields access via column names
53+
instead of indexes.
5854
"""
5955
return self._get_cursor(cursor_factory=pg_extras.DictCursor)
6056

57+
@property
58+
def nt_cursor(self):
59+
"""Named tuple based cursor. It enables to get attributes access via
60+
attributes.
61+
"""
62+
return self._get_cursor(cursor_factory=pg_extras.NamedTupleCursor)
6163

62-
@six.add_metaclass(abc.ABCMeta)
63-
class QuerySet(object):
64-
def __init__(self):
65-
self.request_tokens = []
66-
67-
def execute(self, cursor):
68-
return cursor.execute(' '.join(self.request_tokens))
69-
70-
71-
class SelectRequest(QuerySet):
72-
"""The idea is use builder to select to database:
73-
select('*').from('my_table').where('a>b').order_by('desc').
74-
"""
75-
def __init__(self):
76-
super(SelectRequest, self).__init__()
77-
78-
def select(self, fields=None):
79-
self.request_tokens.append(fields or '*')
80-
return self
81-
82-
def _from(self, table_name):
83-
self.request_tokens.append(table_name)
84-
return self
64+
@property
65+
def available_connections(self):
66+
"""Connection pool available connections
8567
86-
def where(self, raw_condition=None):
87-
if raw_condition is not None:
88-
self.request_tokens.append(raw_condition)
89-
return self
68+
:return: int: number of available connections
69+
"""
70+
return len(self._pool._pool)

pgclient/system_test/Dockerfile

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
FROM postgresql
1+
FROM postgres
2+
MAINTAINER Maksim Ekimovskii <[email protected]>
23

34
# Default user is postgres
4-
ENV POSTGRES_PASSWORD: test
5+
# ENV POSTGRES_PASSWORD: test
56

67
# This dir is supported by official postgresql image
7-
ENV PSQL_ENTRYPOINT /docker-entrypoint-initdb.d
8+
# ENV PSQL_ENTRYPOINT /docker-entrypoint-initdb.d
89

9-
MKDIR $PSQL_ENTRYPOINT
10-
RUN echo "CREATE DATABASE test WITH OWNER postgres;" >> $PSQL_ENTRYPOINT/create_db.sql
11-
12-
#CMD python app.py
10+
RUN echo "CREATE DATABASE test WITH OWNER postgres;" >> $PSQL_ENTRYPOINT/create_db.sql

pgclient/system_test/system_test.py

+22-20
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
)
1111

1212
import unittest
13-
from pgclient.client import DatabaseManager
1413
import random
14+
from pgclient.client import PostgresClient
1515

1616

1717
NAMES = ['Alex', 'Andrea', 'Ashley', 'Casey', 'Chris', 'Dorian', 'Jerry']
1818

1919

20-
class DatabaseManagerSystemTest(unittest.TestCase):
20+
class PostgresClientSystemTest(unittest.TestCase):
2121
DB_USER = 'postgres'
2222
DB_PASSWORD = 'test'
2323
DB_NAME = 'test'
@@ -26,7 +26,12 @@ class DatabaseManagerSystemTest(unittest.TestCase):
2626
def setUp(self):
2727
dsn = 'user={} password={} dbname={} host=localhost'.format(
2828
self.DB_USER, self.DB_PASSWORD, self.DB_NAME)
29-
self.db_manager = DatabaseManager(dsn=dsn, pool_size=10)
29+
try:
30+
self.pg_client = PostgresClient(dsn=dsn, pool_size=10)
31+
except psycopg2.OperationalError as err:
32+
print('Check that postgres docker container is started. '
33+
'Check README for more information')
34+
raise psycopg2.OperationalError(err.message)
3035

3136
try:
3237
self._create_table()
@@ -35,69 +40,66 @@ def setUp(self):
3540
self._create_table()
3641

3742
# Insert 100 entries
38-
with self.db_manager.cursor as cursor:
43+
with self.pg_client.cursor as cursor:
3944
for _ in range(100):
4045
insert_str = "INSERT INTO {} (username) VALUES (%s)".format(
4146
self.TABLE_NAME)
4247
cursor.execute(insert_str, (random.choice(NAMES),))
4348

4449
def _create_table(self):
4550
# Init database with test data
46-
with self.db_manager.cursor as cursor:
51+
with self.pg_client.cursor as cursor:
4752
cursor.execute(
4853
"CREATE TABLE {} "
49-
"(id serial PRIMARY KEY, username VARCHAR NOT NULL );".format(
54+
"(id SERIAL, username VARCHAR NOT NULL );".format(
5055
self.TABLE_NAME))
5156
print('Table {} has been created'.format(self.TABLE_NAME))
5257

5358
def _drop_table(self):
54-
with self.db_manager.cursor as cursor:
59+
with self.pg_client.cursor as cursor:
5560
cursor.execute('DROP TABLE {}'.format(self.TABLE_NAME))
5661
print('Table {} has been dropped'.format(self.TABLE_NAME))
5762

5863
def tearDown(self):
5964
self._drop_table()
6065

6166
def test_cursor(self):
62-
with self.db_manager.cursor as cursor:
67+
with self.pg_client.cursor as cursor:
6368
cursor.execute('SELECT * FROM users')
6469
result_set = cursor.fetchall()
6570
self.assertEqual(len(result_set), 100)
6671

6772
def test_dict_cursor(self):
68-
with self.db_manager.dict_cursor as cursor:
73+
with self.pg_client.dict_cursor as cursor:
6974
cursor.execute('SELECT * FROM users')
70-
result_set = cursor.fetchall()
75+
result_set = cursor.fetchall()
7176
item = result_set[0]
7277
self.assertIn('id', item)
7378
self.assertIn('username', item)
7479
self.assertIn(item['username'], NAMES)
7580

7681
def test_named_tuple_cursor(self):
77-
with self.db_manager.nt_cursor as cursor:
82+
with self.pg_client.nt_cursor as cursor:
7883
cursor.execute('SELECT * FROM users')
79-
result_set = cursor.fetchall()
84+
result_set = cursor.fetchall()
8085
item = result_set[0]
8186
self.assertIsInstance(item.id, int)
8287
self.assertIsInstance(item.username, str)
8388

8489
def test_success_transaction(self):
85-
with self.db_manager.cursor as transaction:
90+
with self.pg_client.cursor as transaction:
8691
insert_str = "INSERT INTO {} (username) VALUES (%s)".format(
8792
self.TABLE_NAME)
8893
transaction.execute(insert_str, (random.choice(NAMES), ))
8994
transaction.execute('SELECT * FROM users')
90-
result_set = transaction.fetchall()
95+
result_set = transaction.fetchall()
9196
self.assertEqual(len(result_set), 101)
9297

9398
def test_rollback_transaction(self):
94-
with self.db_manager.cursor as transaction:
99+
# Insert null username must cause an error
100+
with self.pg_client.cursor as transaction:
95101
with self.assertRaises(psycopg2.DatabaseError) as err:
96102
transaction.execute(
97103
"INSERT INTO {} (username) VALUES (%s)".format(self.TABLE_NAME),
98104
(None, ))
99-
self.assertIn('null value in column', err.exception.message)
100-
101-
102-
if __name__ == '__main__':
103-
unittest.main()
105+
self.assertIn('null value in column', err.exception.message)

0 commit comments

Comments
 (0)