Skip to content

Commit c256c0c

Browse files
committed
huge refactoring, split code into several submodules
1 parent 8434cc3 commit c256c0c

File tree

12 files changed

+803
-681
lines changed

12 files changed

+803
-681
lines changed

MANIFEST

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,12 @@
22
setup.cfg
33
setup.py
44
testgres/__init__.py
5-
testgres/testgres.py
5+
testgres/api.py
6+
testgres/backup.py
7+
testgres/config.py
8+
testgres/connection.py
9+
testgres/consts.py
10+
testgres/exceptions.py
11+
testgres/logger.py
12+
testgres/node.py
13+
testgres/utils.py

testgres/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,20 @@
1-
from .testgres import *
1+
from .api import get_new_node
2+
from .backup import NodeBackup
3+
from .config import TestgresConfig, configure_testgres
4+
5+
from .connection import \
6+
IsolationLevel, \
7+
NodeConnection, \
8+
InternalError, \
9+
ProgrammingError
10+
11+
from .exceptions import *
12+
from .node import NodeStatus, PostgresNode
13+
14+
from .utils import \
15+
reserve_port, \
16+
release_port, \
17+
bound_ports, \
18+
get_bin_path, \
19+
get_pg_config, \
20+
get_pg_version

testgres/api.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# coding: utf-8
2+
"""
3+
Testing framework for PostgreSQL and its extensions
4+
5+
This module was created under influence of Postgres TAP test feature
6+
(PostgresNode.pm module). It can manage Postgres clusters: initialize,
7+
edit configuration files, start/stop cluster, execute queries. The
8+
typical flow may look like:
9+
10+
with get_new_node('test') as node:
11+
node.init()
12+
node.start()
13+
result = node.psql('postgres', 'SELECT 1')
14+
print(result)
15+
node.stop()
16+
17+
Or:
18+
19+
with get_new_node('node1') as node1:
20+
node1.init().start()
21+
with node1.backup() as backup:
22+
with backup.spawn_primary('node2') as node2:
23+
res = node2.start().execute('postgres', 'select 2')
24+
print(res)
25+
26+
Copyright (c) 2016, Postgres Professional
27+
"""
28+
29+
from .node import PostgresNode
30+
31+
32+
def get_new_node(name, base_dir=None, use_logging=False):
33+
"""
34+
Create a new node (select port automatically).
35+
36+
Args:
37+
name: node's name.
38+
base_dir: path to node's data directory.
39+
use_logging: enable python logging.
40+
41+
Returns:
42+
An instance of PostgresNode.
43+
"""
44+
45+
return PostgresNode(name=name, base_dir=base_dir, use_logging=use_logging)

testgres/backup.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# coding: utf-8
2+
3+
import os
4+
import shutil
5+
import tempfile
6+
7+
from .consts import \
8+
DATA_DIR as _DATA_DIR, \
9+
BACKUP_LOG_FILE as _BACKUP_LOG_FILE, \
10+
DEFAULT_XLOG_METHOD as _DEFAULT_XLOG_METHOD
11+
12+
from .exceptions import \
13+
BackupException
14+
15+
from .utils import \
16+
default_username as _default_username, \
17+
execute_utility as _execute_utility, \
18+
explain_exception as _explain_exception
19+
20+
21+
class NodeBackup(object):
22+
"""
23+
Smart object responsible for backups
24+
"""
25+
26+
@property
27+
def log_file(self):
28+
return os.path.join(self.base_dir, _BACKUP_LOG_FILE)
29+
30+
def __init__(self,
31+
node,
32+
base_dir=None,
33+
username=None,
34+
xlog_method=_DEFAULT_XLOG_METHOD):
35+
36+
if not node.status():
37+
raise BackupException('Node must be running')
38+
39+
# Set default arguments
40+
username = username or _default_username()
41+
base_dir = base_dir or tempfile.mkdtemp()
42+
43+
# public
44+
self.original_node = node
45+
self.base_dir = base_dir
46+
47+
# private
48+
self._available = True
49+
50+
data_dir = os.path.join(self.base_dir, _DATA_DIR)
51+
_params = [
52+
"-D{}".format(data_dir), "-p{}".format(node.port),
53+
"-U{}".format(username), "-X{}".format(xlog_method)
54+
]
55+
_execute_utility("pg_basebackup", _params, self.log_file)
56+
57+
def __enter__(self):
58+
return self
59+
60+
def __exit__(self, type, value, traceback):
61+
self.cleanup()
62+
63+
def _prepare_dir(self, destroy):
64+
"""
65+
Provide a data directory for a copy of node.
66+
67+
Args:
68+
destroy: should we convert this backup into a node?
69+
70+
Returns:
71+
Path to data directory.
72+
"""
73+
74+
if not self._available:
75+
raise BackupException('Backup is exhausted')
76+
77+
# Do we want to use this backup several times?
78+
available = not destroy
79+
80+
if available:
81+
dest_base_dir = tempfile.mkdtemp()
82+
83+
data1 = os.path.join(self.base_dir, _DATA_DIR)
84+
data2 = os.path.join(dest_base_dir, _DATA_DIR)
85+
86+
try:
87+
# Copy backup to new data dir
88+
shutil.copytree(data1, data2)
89+
except Exception as e:
90+
raise BackupException(_explain_exception(e))
91+
else:
92+
dest_base_dir = self.base_dir
93+
94+
# Is this backup exhausted?
95+
self._available = available
96+
97+
# Return path to new node
98+
return dest_base_dir
99+
100+
def spawn_primary(self, name, destroy=True, use_logging=False):
101+
"""
102+
Create a primary node from a backup.
103+
104+
Args:
105+
name: name for a new node.
106+
destroy: should we convert this backup into a node?
107+
use_logging: enable python logging.
108+
109+
Returns:
110+
New instance of PostgresNode.
111+
"""
112+
113+
base_dir = self._prepare_dir(destroy)
114+
115+
# Build a new PostgresNode
116+
from .node import PostgresNode
117+
node = PostgresNode(
118+
name=name,
119+
base_dir=base_dir,
120+
master=self.original_node,
121+
use_logging=use_logging)
122+
123+
# New nodes should always remove dir tree
124+
node._should_rm_dirs = True
125+
126+
node.append_conf("postgresql.conf", "\n")
127+
node.append_conf("postgresql.conf", "port = {}".format(node.port))
128+
129+
return node
130+
131+
def spawn_replica(self, name, destroy=True, use_logging=False):
132+
"""
133+
Create a replica of the original node from a backup.
134+
135+
Args:
136+
name: name for a new node.
137+
destroy: should we convert this backup into a node?
138+
use_logging: enable python logging.
139+
140+
Returns:
141+
New instance of PostgresNode.
142+
"""
143+
144+
node = self.spawn_primary(name, destroy, use_logging=use_logging)
145+
node._create_recovery_conf(self.original_node)
146+
147+
return node
148+
149+
def cleanup(self):
150+
if self._available:
151+
shutil.rmtree(self.base_dir, ignore_errors=True)
152+
self._available = False

testgres/config.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# coding: utf-8
2+
3+
4+
class TestgresConfig:
5+
"""
6+
Global config (override default settings).
7+
"""
8+
9+
# shall we cache pg_config results?
10+
cache_pg_config = True
11+
12+
# shall we use cached initdb instance?
13+
cache_initdb = True
14+
15+
# shall we create a temp dir for cached initdb?
16+
cached_initdb_dir = None
17+
18+
# shall we remove EVERYTHING (including logs)?
19+
node_cleanup_full = True
20+
21+
22+
def configure_testgres(**options):
23+
"""
24+
Configure testgres.
25+
Look at TestgresConfig to check what can be changed.
26+
"""
27+
28+
for key, option in options.items():
29+
setattr(TestgresConfig, key, option)

testgres/connection.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# coding: utf-8
2+
3+
# we support both pg8000 and psycopg2
4+
try:
5+
import psycopg2 as pglib
6+
except ImportError:
7+
try:
8+
import pg8000 as pglib
9+
except ImportError:
10+
raise ImportError("You must have psycopg2 or pg8000 modules installed")
11+
12+
from enum import Enum
13+
14+
from .exceptions import QueryException
15+
from .utils import default_username as _default_username
16+
17+
# export these exceptions
18+
InternalError = pglib.InternalError
19+
ProgrammingError = pglib.ProgrammingError
20+
21+
22+
class IsolationLevel(Enum):
23+
"""
24+
Transaction isolation level for NodeConnection
25+
"""
26+
27+
ReadUncommitted, ReadCommitted, RepeatableRead, Serializable = range(4)
28+
29+
30+
class NodeConnection(object):
31+
"""
32+
Transaction wrapper returned by Node
33+
"""
34+
35+
def __init__(self,
36+
parent_node,
37+
dbname,
38+
host="127.0.0.1",
39+
username=None,
40+
password=None):
41+
42+
# Use default user if not specified
43+
username = username or _default_username()
44+
45+
self.parent_node = parent_node
46+
47+
self.connection = pglib.connect(
48+
database=dbname,
49+
user=username,
50+
port=parent_node.port,
51+
host=host,
52+
password=password)
53+
54+
self.cursor = self.connection.cursor()
55+
56+
def __enter__(self):
57+
return self
58+
59+
def __exit__(self, type, value, traceback):
60+
self.close()
61+
62+
def begin(self, isolation_level=IsolationLevel.ReadCommitted):
63+
# yapf: disable
64+
levels = [
65+
'read uncommitted',
66+
'read committed',
67+
'repeatable read',
68+
'serializable'
69+
]
70+
71+
# Check if level is an IsolationLevel
72+
if (isinstance(isolation_level, IsolationLevel)):
73+
74+
# Get index of isolation level
75+
level_idx = isolation_level.value
76+
assert(level_idx in range(4))
77+
78+
# Replace isolation level with its name
79+
isolation_level = levels[level_idx]
80+
81+
else:
82+
# Get name of isolation level
83+
level_str = str(isolation_level).lower()
84+
85+
# Validate level string
86+
if level_str not in levels:
87+
error = 'Invalid isolation level "{}"'
88+
raise QueryException(error.format(level_str))
89+
90+
# Replace isolation level with its name
91+
isolation_level = level_str
92+
93+
# Set isolation level
94+
cmd = 'SET TRANSACTION ISOLATION LEVEL {}'
95+
self.cursor.execute(cmd.format(isolation_level))
96+
97+
return self
98+
99+
def commit(self):
100+
self.connection.commit()
101+
102+
return self
103+
104+
def rollback(self):
105+
self.connection.rollback()
106+
107+
return self
108+
109+
def execute(self, query, *args):
110+
self.cursor.execute(query, args)
111+
112+
try:
113+
res = self.cursor.fetchall()
114+
115+
# pg8000 might return tuples
116+
if isinstance(res, tuple):
117+
res = [tuple(t) for t in res]
118+
119+
return res
120+
except Exception:
121+
return None
122+
123+
def close(self):
124+
self.cursor.close()
125+
self.connection.close()

0 commit comments

Comments
 (0)