pgmonkey 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. pgmonkey-0.0.1/LICENSE +21 -0
  2. pgmonkey-0.0.1/PKG-INFO +42 -0
  3. pgmonkey-0.0.1/README.md +1 -0
  4. pgmonkey-0.0.1/pyproject.toml +38 -0
  5. pgmonkey-0.0.1/setup.cfg +4 -0
  6. pgmonkey-0.0.1/src/cli/__init__.py +0 -0
  7. pgmonkey-0.0.1/src/cli/cli.py +44 -0
  8. pgmonkey-0.0.1/src/cli/cli_pg_server_config_subparser.py +20 -0
  9. pgmonkey-0.0.1/src/cli/cli_pgconfig_subparser.py +41 -0
  10. pgmonkey-0.0.1/src/cli/cli_settings_subparser.py +20 -0
  11. pgmonkey-0.0.1/src/cli/cli_toplevel_parser.py +35 -0
  12. pgmonkey-0.0.1/src/pgmonkey/__init__.py +1 -0
  13. pgmonkey-0.0.1/src/pgmonkey/common/__init__.py +0 -0
  14. pgmonkey-0.0.1/src/pgmonkey/common/config/__init__.py +0 -0
  15. pgmonkey-0.0.1/src/pgmonkey/common/utils/__init__.py +0 -0
  16. pgmonkey-0.0.1/src/pgmonkey/common/utils/pathutils.py +83 -0
  17. pgmonkey-0.0.1/src/pgmonkey/connections/__init__.py +0 -0
  18. pgmonkey-0.0.1/src/pgmonkey/connections/base.py +17 -0
  19. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/__init__.py +1 -0
  20. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/async_connection.py +47 -0
  21. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/async_pool_connection.py +43 -0
  22. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/base_connection.py +7 -0
  23. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/normal_connection.py +38 -0
  24. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/pool_connection.py +52 -0
  25. pgmonkey-0.0.1/src/pgmonkey/connections/postgres/postgres_connection_factory.py +41 -0
  26. pgmonkey-0.0.1/src/pgmonkey/managers/__init__.py +0 -0
  27. pgmonkey-0.0.1/src/pgmonkey/managers/pg_server_config_manager.py +24 -0
  28. pgmonkey-0.0.1/src/pgmonkey/managers/pgconfig_manager.py +72 -0
  29. pgmonkey-0.0.1/src/pgmonkey/managers/pgconnection_manager.py +34 -0
  30. pgmonkey-0.0.1/src/pgmonkey/managers/settings_manager.py +22 -0
  31. pgmonkey-0.0.1/src/pgmonkey/managers/toplevel_manager.py +13 -0
  32. pgmonkey-0.0.1/src/pgmonkey/serversettings/postgres_server_config_generator.py +103 -0
  33. pgmonkey-0.0.1/src/pgmonkey.egg-info/PKG-INFO +42 -0
  34. pgmonkey-0.0.1/src/pgmonkey.egg-info/SOURCES.txt +38 -0
  35. pgmonkey-0.0.1/src/pgmonkey.egg-info/dependency_links.txt +1 -0
  36. pgmonkey-0.0.1/src/pgmonkey.egg-info/entry_points.txt +2 -0
  37. pgmonkey-0.0.1/src/pgmonkey.egg-info/requires.txt +3 -0
  38. pgmonkey-0.0.1/src/pgmonkey.egg-info/top_level.txt +4 -0
  39. pgmonkey-0.0.1/src/tests/__init__.py +0 -0
  40. pgmonkey-0.0.1/src/tests/database_integration_test.py +26 -0
pgmonkey-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RexBytes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: pgmonkey
3
+ Version: 0.0.1
4
+ Summary: A tool to assist with postgresql database connections
5
+ Author-email: Good Boy <pythonic@rexbytes.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 RexBytes
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/RexBytes/pgmonkey
29
+ Project-URL: Bug Tracker, https://github.com/RexBytes/pgmonkey
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Topic :: Database
34
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
+ Requires-Python: >=3.12
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: psycopg[binary]>=3.2.1
39
+ Requires-Dist: psycopg_pool>=3.2.2
40
+ Requires-Dist: PyYAML>=6.0.2
41
+
42
+ # pgmonkey
@@ -0,0 +1 @@
1
+ # pgmonkey
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=65.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pgmonkey"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Good Boy", email="pythonic@rexbytes.com" },
10
+ ]
11
+ description = "A tool to assist with postgresql database connections"
12
+ readme = "README.md"
13
+ license = { file="LICENSE" }
14
+ requires-python = ">=3.12"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.12",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Database",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ dependencies = [
24
+ "psycopg[binary]>=3.2.1",
25
+ "psycopg_pool>=3.2.2",
26
+ "PyYAML>=6.0.2",
27
+ ]
28
+
29
+ [tool.setuptools.package-data]
30
+ pglinker = ["app_settings.yaml", "common/templates/*"]
31
+
32
+ [project.urls]
33
+ "Homepage" = "https://github.com/RexBytes/pgmonkey"
34
+ "Bug Tracker" = "https://github.com/RexBytes/pgmonkey"
35
+
36
+ [project.scripts]
37
+ pgmonkey="cli.cli:main"
38
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,44 @@
1
+ from .cli_toplevel_parser import cli_toplevel_parser
2
+ from .cli_settings_subparser import cli_settings_subparser
3
+ from .cli_pgconfig_subparser import cli_pgconfig_subparser
4
+ from .cli_pg_server_config_subparser import cli_pg_server_config_subparser
5
+
6
+
7
+ class CLI:
8
+ def __init__(self):
9
+ # We need the main top level parser.
10
+ self.parser = cli_toplevel_parser()
11
+ # We attach a 'sub-parser container' to hold all subparsers, e.g. the subparser that handles settings.
12
+ self.subparsers = self.parser.add_subparsers(title="commands", dest="command", help="Available commands")
13
+ # We start populating the 'sub-parser container' with subparsers.
14
+ cli_settings_subparser(self.subparsers)
15
+ cli_pgconfig_subparser(self.subparsers)
16
+ cli_pg_server_config_subparser(self.subparsers)
17
+
18
+ def run(self):
19
+ # Parse all arguments from the command line into argparse.
20
+ args = self.parser.parse_args()
21
+ # This bit here feels like magic, let me explain.
22
+ # You already have defined parser and subparser logic which the argparse module understands.
23
+ # If you give it say "pgmonkey settings --helloworld" it knows it needs to looks at your
24
+ # settings subparser, and it knows you have given it --helloworld argument.
25
+ # It looks at all the arguments you have given, and the associated default
26
+ # arguments, in this case func=settings_subparser_handle.
27
+ #
28
+ # The following if statement merely checks if the argument func has been populated,
29
+ # it then calls the function and passes in all the other arguments.
30
+ # The argument helloworld is detected and acted on by the function settings_subparser_handle()
31
+ # This is a very flexible implementation, which is why it feels like magic.
32
+ if hasattr(args, 'func'):
33
+ args.func(args)
34
+ else:
35
+ self.parser.print_help()
36
+
37
+
38
+ def main():
39
+ cli = CLI()
40
+ cli.run()
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
@@ -0,0 +1,20 @@
1
+ from pgmonkey.managers.pg_server_config_manager import PGServerConfigManager
2
+
3
+
4
+ def cli_pg_server_config_subparser(subparsers):
5
+ pg_server_config_manager = PGServerConfigManager()
6
+
7
+ pg_server_config_parser = subparsers.add_parser('pgserverconfig',
8
+ help='Generate suggested server configuration entries')
9
+
10
+ pg_server_config_parser.add_argument('--filepath', required=True,
11
+ help='Path to the config you want settings generated.')
12
+ pg_server_config_parser.set_defaults(func=pg_server_config_create_handler,
13
+ pg_server_config_manager=pg_server_config_manager)
14
+
15
+
16
+ def pg_server_config_create_handler(args):
17
+ pg_server_config_manager = args.pg_server_config_manager
18
+
19
+ if args.filepath:
20
+ pg_server_config_manager.get_server_config(args.filepath)
@@ -0,0 +1,41 @@
1
+ from pgmonkey.managers.pgconfig_manager import PGConfigManager
2
+ from pathlib import Path
3
+
4
+
5
+ def cli_pgconfig_subparser(subparsers):
6
+ pgconfig_manager = PGConfigManager()
7
+
8
+
9
+ pgconfig_parser = subparsers.add_parser('pgconfig', help='Manage database configurations')
10
+ # Note, the following line is adding a subparser to this subparser which is allowed.
11
+ pgconfig_subparsers = pgconfig_parser.add_subparsers(dest='pgconfig_command', help='pgconfig commands')
12
+
13
+ # The "create" subcommand
14
+ create_parser = pgconfig_subparsers.add_parser('create', help='Create a new database configuration file.')
15
+ create_parser.add_argument('--type', choices=['pg'], default='pg', required=False,
16
+ help='Database type for the configuration template. Default is "pg".')
17
+ create_parser.add_argument('--filepath', required=True,
18
+ help='Path to where you want your configuration file created.')
19
+ create_parser.set_defaults(func=pgconfig_create_handler, pgconfig_manager=pgconfig_manager)
20
+
21
+ # The "test" subcommand
22
+ test_parser = pgconfig_subparsers.add_parser('test',
23
+ help='Test the database connection using a configuration file.')
24
+ test_parser.add_argument('--filepath', required=True, help='Path to the configuration file you want to test.')
25
+ test_parser.set_defaults(func=pgconfig_test_handler, pgconfig_manager=pgconfig_manager)
26
+
27
+
28
+
29
+ def pgconfig_create_handler(args):
30
+ pgconfig_manager = args.pgconfig_manager
31
+ filepath = Path(args.filepath)
32
+ if filepath.exists():
33
+ print(f'Configuration file already exists: {filepath}\nEdit this file to update settings.')
34
+ else:
35
+ config_template_text = pgconfig_manager.get_config_template_text(args.type)
36
+ pgconfig_manager.write_config_template_text(filepath, config_template_text)
37
+
38
+
39
+ def pgconfig_test_handler(args):
40
+ pgconfig_manager = args.pgconfig_manager
41
+ pgconfig_manager.test_connection(args.filepath)
@@ -0,0 +1,20 @@
1
+ import argparse
2
+ from pgmonkey.managers.settings_manager import SettingsManager
3
+
4
+
5
+ def cli_settings_subparser(subparsers):
6
+ # Create a subparser for the settings command
7
+ subparser = subparsers.add_parser('settings', help='Manage application settings.')
8
+
9
+ # Add arguments specific to the settings management
10
+ subparser.add_argument("--helloworld", type=str, help="Place holder for settings subparser")
11
+
12
+ # Set the default function to call with parsed arguments
13
+ subparser.set_defaults(func=settings_subparser_handle)
14
+
15
+
16
+ def settings_subparser_handle(args):
17
+ settings_manager = SettingsManager()
18
+
19
+ if args.helloworld:
20
+ settings_manager.print_hello_world()
@@ -0,0 +1,35 @@
1
+ import argparse
2
+ from pgmonkey.managers.toplevel_manager import ToplevelManager
3
+
4
+
5
+ def cli_toplevel_parser():
6
+ parser = top_level_parser_init()
7
+ parser = top_level_parser_arguments(parser)
8
+ return parser
9
+
10
+
11
+ def top_level_parser_init():
12
+ # Let's create the main top-level parser,
13
+ parser = argparse.ArgumentParser(
14
+ # prog: whenever the automatically generated help messages are displayed it will
15
+ # use the string value to refer to the application.
16
+ prog='pgmonkey',
17
+ # description: this is the description of your application.
18
+ description="pgmonkey: A tool to assist with database connections and manage data."
19
+ )
20
+
21
+ return parser
22
+
23
+
24
+ def top_level_parser_arguments(parser):
25
+ parser.add_argument(
26
+ # add --version argument to the top level parser.
27
+ '--version',
28
+ # The 'version' action is a builtin argparse action specific for versioning info.
29
+ action='version',
30
+ # Construct a string with version information that will be returned.
31
+ # Here we use the VersionManager class.
32
+ # We also use the prog string we defined in the top-level parser creation above.
33
+ version=f"%(prog)s {ToplevelManager.get_package_version('pgmonkey')}"
34
+ )
35
+ return parser
@@ -0,0 +1 @@
1
+ from .managers.pgconnection_manager import PGConnectionManager
File without changes
File without changes
File without changes
@@ -0,0 +1,83 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ class PathUtils:
6
+ @staticmethod
7
+ def construct_path(path_elements):
8
+ """Construct a path from a list of elements, handling '~', '', '/', and '\\'."""
9
+ if not path_elements:
10
+ return Path()
11
+
12
+ # Start with handling the first element specially if it indicates a root or home directory
13
+ first_elem = path_elements[0]
14
+ if first_elem in ['~', '', '/', '\\']:
15
+ if first_elem == '~':
16
+ # Path relative to user's home directory
17
+ base_path = Path.home()
18
+ elif first_elem in ['', '/', '\\']:
19
+ # Absolute path starting from root
20
+ base_path = Path('/')
21
+ return base_path.joinpath(*path_elements[1:])
22
+ elif ':' in first_elem and len(first_elem) == 2 and first_elem[1] == ':':
23
+ # Windows-specific: path starts with a drive letter
24
+ return Path(first_elem).joinpath(*path_elements[1:])
25
+ else:
26
+ # Normal path construction
27
+ return Path(*path_elements)
28
+
29
+ @staticmethod
30
+ def deconstruct_path(path):
31
+ """Deconstruct a Path object into a list of its components, considering special cases."""
32
+ components = []
33
+
34
+ # Handle absolute paths, including Windows-specific drive letters
35
+ if path.is_absolute():
36
+ if path.drive:
37
+ components.append(path.drive)
38
+ else:
39
+ components.append('')
40
+ components.extend(path.parts[1:])
41
+ elif path.home() in path.parents:
42
+ # Handle paths relative to the home directory
43
+ components.append('~')
44
+ components.extend(path.relative_to(Path.home()).parts)
45
+ else:
46
+ # Handle relative paths
47
+ components.extend(path.parts)
48
+
49
+ return components
50
+
51
+
52
+ # Example Usage:
53
+ if __name__ == "__main__":
54
+ # Test cases for constructing paths
55
+ print("Testing Path Construction:")
56
+ test_elements = [
57
+ (['~', 'projects', 'example'], "Home directory relative"),
58
+ (['/', 'usr', 'bin'], "Root directory"),
59
+ (['', 'usr', 'bin'], "Absolute path from root"),
60
+ (['C:', 'Users', 'example'], "Windows drive letter"),
61
+ (['..', 'another_folder'], "Relative path"),
62
+ (['usr', 'bin'], "Additional relative path"),
63
+ (['~', '.rexdblinker', 'connectionconfigs'], "RexPath")
64
+ ]
65
+ for elements, description in test_elements:
66
+ path = PathUtils.construct_path(elements)
67
+ print(f"{description} Path: {path}")
68
+
69
+ # Test cases for deconstructing paths
70
+ print("\nTesting Path Deconstruction:")
71
+ test_paths = [
72
+ (Path.home().joinpath('projects', 'example'), "Home directory relative"),
73
+ (Path('/usr/bin'), "Root directory"),
74
+ (Path('usr/bin'), "Relative path from current directory"), # Corrected description
75
+ (Path('C:/Users/example'), "Windows drive letter"),
76
+ (Path('../another_folder'), "Relative path")
77
+ ]
78
+ for path, description in test_paths:
79
+ path_list = PathUtils.deconstruct_path(path)
80
+ print(f"{description} Path List: {path_list}")
81
+
82
+
83
+
File without changes
@@ -0,0 +1,17 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class BaseConnection(ABC):
4
+ @abstractmethod
5
+ def connect(self):
6
+ """Establish a database connection."""
7
+ pass
8
+
9
+ @abstractmethod
10
+ def test_connection(self):
11
+ """Test the database connection."""
12
+ pass
13
+
14
+ @abstractmethod
15
+ def disconnect(self):
16
+ """Close the database connection."""
17
+ pass
@@ -0,0 +1 @@
1
+ from .postgres_connection_factory import PostgresConnectionFactory
@@ -0,0 +1,47 @@
1
+ from psycopg import AsyncConnection, OperationalError
2
+ from .base_connection import BaseConnection
3
+
4
+
5
+ class PGAsyncConnection(BaseConnection):
6
+ def __init__(self, config, post_connect_async_settings=None):
7
+ super().__init__()
8
+ self.config = config
9
+ # This dictionary can contain settings to be applied after the connection is established
10
+ self.post_connect_async_settings = post_connect_async_settings or {}
11
+ self.connection: AsyncConnection = None
12
+
13
+ async def connect(self):
14
+ """Establishes an asynchronous database connection."""
15
+ if self.connection is None or self.connection.closed:
16
+ self.connection = await AsyncConnection.connect(**self.config)
17
+ await self.apply_post_connect_settings()
18
+
19
+ async def apply_post_connect_settings(self):
20
+ """Applies any settings that need to be set after the connection is established."""
21
+ for setting, value in self.post_connect_async_settings.items():
22
+ # This example assumes settings can be applied directly as attributes
23
+ # Adjust this method based on the actual async settings and their required handling
24
+ setattr(self.connection, setting, value)
25
+
26
+ async def __aenter__(self):
27
+ if self.connection is None or self.connection.closed:
28
+ await self.connect()
29
+ return self
30
+
31
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
32
+ await self.disconnect()
33
+
34
+ async def test_connection(self):
35
+ """Tests the asynchronous database connection."""
36
+ if self.connection is None or self.connection.closed:
37
+ await self.connect()
38
+ async with self.connection.cursor() as cur:
39
+ await cur.execute('SELECT 1;')
40
+ result = await cur.fetchone()
41
+ print("Async connection successful: ", result)
42
+
43
+ async def disconnect(self):
44
+ """Closes the asynchronous database connection."""
45
+ if self.connection and not self.connection.closed:
46
+ await self.connection.close()
47
+ self.connection = None
@@ -0,0 +1,43 @@
1
+ from psycopg_pool import AsyncConnectionPool
2
+ from .base_connection import PostgresBaseConnection
3
+
4
+
5
+ class PGAsyncPoolConnection(PostgresBaseConnection):
6
+ def __init__(self, config, pool_settings=None):
7
+ super().__init__() # Call super if the base class has an __init__ method
8
+ self.config = config
9
+ self.pool_settings = pool_settings or {}
10
+ self.pool = None
11
+
12
+ def construct_dsn(self):
13
+ """Assuming self.config directly contains connection info as a dict."""
14
+ # This assumes all keys in self.config are for the connection,
15
+ # adjust if your config includes other types of settings.
16
+ return " ".join([f"{k}={v}" for k, v in self.config.items()])
17
+
18
+ async def connect(self):
19
+ dsn = self.construct_dsn()
20
+ # Initialize AsyncConnectionPool with DSN and any pool-specific settings
21
+ self.pool = AsyncConnectionPool(conninfo=dsn, **self.pool_settings)
22
+ await self.pool.open()
23
+
24
+ async def __aenter__(self):
25
+ if not self.pool:
26
+ await self.connect()
27
+ return self
28
+
29
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
30
+ await self.disconnect()
31
+
32
+ async def test_connection(self):
33
+ if not self.pool:
34
+ await self.connect()
35
+ async with self.pool.connection() as conn:
36
+ async with conn.cursor() as cur:
37
+ await cur.execute('SELECT 1;')
38
+ print("Async pool connection successful: ", await cur.fetchone())
39
+
40
+ async def disconnect(self):
41
+ if self.pool:
42
+ await self.pool.close()
43
+ self.pool = None
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+ from ..base import BaseConnection # Assuming base.py contains BaseConnection and is in the same directory level
3
+
4
+
5
+ class PostgresBaseConnection(BaseConnection, ABC):
6
+ # PostgreSQL-specific shared behavior (if any)
7
+ pass
@@ -0,0 +1,38 @@
1
+ from psycopg import connect, OperationalError # This imports psycopg3, assuming you have installed 'psycopg' version 3+
2
+ from .base_connection import PostgresBaseConnection
3
+
4
+
5
+ class PGNormalConnection(PostgresBaseConnection):
6
+ def __init__(self, config):
7
+ # The configuration dict should contain connection parameters like dbname, user, password, etc.
8
+ self.config = config
9
+ self.connection = None
10
+
11
+ def connect(self):
12
+ # Establishes a synchronous connection to the PostgreSQL server.
13
+ # The 'connect' function is used both in psycopg2 and psycopg3 for this purpose.
14
+ self.connection = connect(**self.config)
15
+
16
+
17
+ def test_connection(self):
18
+ try:
19
+ with self.connection.cursor() as cur:
20
+ cur.execute('SELECT 1;') # Execute a simple query to test the connection.
21
+ print("Connection successful: ", cur.fetchone())
22
+ except OperationalError as e:
23
+ print(f"Connection failed: {e}")
24
+
25
+ def disconnect(self):
26
+ # Closes the connection to the database.
27
+ if self.connection:
28
+ self.connection.close()
29
+
30
+ def __enter__(self):
31
+ # Ensures the connection is established when entering the context.
32
+ if not self.connection:
33
+ self.connect()
34
+ return self
35
+
36
+ def __exit__(self, exc_type, exc_val, exc_tb):
37
+ # Closes the connection when exiting the context.
38
+ self.disconnect()
@@ -0,0 +1,52 @@
1
+ from psycopg_pool import ConnectionPool
2
+ from psycopg import OperationalError
3
+ # Assuming PostgresBaseConnection is correctly implemented elsewhere
4
+ from .base_connection import PostgresBaseConnection
5
+
6
+
7
+ class PGPoolConnection(PostgresBaseConnection):
8
+ def __init__(self, config, pool_settings=None):
9
+ super().__init__() # Initialize the base class, if necessary
10
+ self.config = config
11
+ self.pool_settings = pool_settings or {}
12
+ # Directly pass connection parameters and pool settings to ConnectionPool
13
+ self.pool = ConnectionPool(conninfo=self.construct_conninfo(self.config), **self.pool_settings)
14
+
15
+ @staticmethod
16
+ def construct_conninfo(config):
17
+ """Constructs a connection info string from the config dictionary, excluding pool settings."""
18
+ # Filter out 'pool_settings' and any other non-connection parameters
19
+ conn_params = {k: v for k, v in config.items() if k not in ['pool_settings'] and v is not None}
20
+ # Construct and return the connection info string
21
+ return " ".join([f"{k}={v}" for k, v in conn_params.items()])
22
+
23
+
24
+ def test_connection(self):
25
+ """Tests a connection from the pool."""
26
+ try:
27
+ with self.pool.connection() as conn:
28
+ with conn.cursor() as cur:
29
+ cur.execute('SELECT 1;')
30
+ result = cur.fetchone()
31
+ print("Pool connection successful: ", result)
32
+ except OperationalError as e:
33
+ print(f"Connection failed: {e}")
34
+
35
+ def disconnect(self):
36
+ """Closes all connections in the pool."""
37
+ if self.pool:
38
+ self.pool.close()
39
+ self.pool = None
40
+
41
+ def connect(self):
42
+ # This method is implemented to satisfy the interface of the abstract base class.
43
+ # The connection pool is initialized in the constructor, so no action is needed here.
44
+ pass
45
+
46
+ def __enter__(self):
47
+ # Return self to make it possible to use 'with PGPoolConnection(config) as conn:'
48
+ return self
49
+
50
+ def __exit__(self, exc_type, exc_val, exc_tb):
51
+ # Ensure the pool is properly closed when exiting the context
52
+ self.disconnect()
@@ -0,0 +1,41 @@
1
+ from pgmonkey.connections.postgres.normal_connection import PGNormalConnection
2
+ from pgmonkey.connections.postgres.pool_connection import PGPoolConnection
3
+ from pgmonkey.connections.postgres.async_connection import PGAsyncConnection
4
+ from pgmonkey.connections.postgres.async_pool_connection import PGAsyncPoolConnection
5
+
6
+
7
+ class PostgresConnectionFactory:
8
+ def __init__(self, config):
9
+ self.connection_type = config['postgresql']['connection_type']
10
+ self.config = self.filter_config(config['postgresql']['connection_settings'])
11
+ self.pool_settings = config['postgresql'].get('pool_settings', {})
12
+ self.async_settings = config['postgresql'].get('async_settings', {})
13
+ self.async_pool_settings = config['postgresql'].get('async_pool_settings', {})
14
+
15
+ @staticmethod
16
+ def filter_config(config):
17
+ # List of valid psycopg connection parameters
18
+ valid_keys = ['user', 'password', 'host', 'port', 'dbname', 'sslmode', 'sslcert', 'sslkey', 'sslrootcert',
19
+ 'connect_timeout', 'application_name', 'keepalives', 'keepalives_idle', 'keepalives_interval',
20
+ 'keepalives_count']
21
+ # Filter the config dictionary to include only the valid keys
22
+ return {key: config[key] for key in valid_keys if key in config}
23
+
24
+ def get_connection(self):
25
+ # print(self.config)
26
+ connection_type = self.connection_type
27
+
28
+ if connection_type == 'normal':
29
+ # Only connection_settings are needed for a normal connection
30
+ return PGNormalConnection(self.config)
31
+ elif connection_type == 'pool':
32
+ # Merge connection_settings with pool_settings
33
+ return PGPoolConnection(self.config, self.pool_settings)
34
+ elif connection_type == 'async':
35
+ # Merge connection_settings with async_settings
36
+ return PGAsyncConnection(self.config, post_connect_async_settings=self.async_settings)
37
+ elif connection_type == 'async_pool':
38
+ # Merge connection_settings with async_pool_settings
39
+ return PGAsyncPoolConnection(config=self.config, pool_settings=self.async_pool_settings)
40
+ else:
41
+ raise ValueError(f"Unsupported connection type: {connection_type}")
File without changes
@@ -0,0 +1,24 @@
1
+ import yaml
2
+ from pgmonkey.serversettings.postgres_server_config_generator import PostgresServerConfigGenerator
3
+
4
+ class PGServerConfigManager:
5
+ def __init__(self):
6
+ pass
7
+
8
+ def get_server_config(self, config_file_path):
9
+ # Need to detect the database type from the config file.
10
+ with open(config_file_path, 'r') as f:
11
+ config_data_dictionary = yaml.safe_load(f)
12
+ database_type = next(iter(config_data_dictionary))
13
+
14
+ if database_type == 'postgresql':
15
+ #print("postgres detected")
16
+ self.get_postgres_server_config(config_file_path)
17
+ else:
18
+ raise ValueError(f"Unsupported database type: {database_type}")
19
+
20
+ def get_postgres_server_config(self, config_file_path):
21
+ postgresconfiggenerator = PostgresServerConfigGenerator(config_file_path)
22
+ postgresconfiggenerator.generate_pg_hba_entry()
23
+ postgresconfiggenerator.generate_pg_hba_entry()
24
+ postgresconfiggenerator.print_configurations()
@@ -0,0 +1,72 @@
1
+ from pathlib import Path
2
+ import yaml
3
+ from importlib import resources
4
+ from tests.database_integration_test import DatabaseIntegrationTest
5
+ import asyncio
6
+ from .settings_manager import SettingsManager
7
+ from pgmonkey.common.utils.pathutils import PathUtils
8
+
9
+
10
+ class PGConfigManager:
11
+ def __init__(self):
12
+ self.path_utils = PathUtils()
13
+ self.settings_manager = SettingsManager()
14
+ self.templates = {'pg': 'postgres.yaml'}
15
+
16
+ def load_template(self, template_filename):
17
+ """Load a YAML configuration template from a file within the package."""
18
+ with resources.files(f"{self.settings_manager.settings['appPackageName']}.common.templates").joinpath(
19
+ template_filename) as path, \
20
+ resources.as_file(path) as config_file, \
21
+ open(config_file, 'r') as file:
22
+ return yaml.safe_load(file)
23
+
24
+ def get_config_template(self, database_type):
25
+ """Retrieve the configuration template based on the database type."""
26
+ if database_type in self.templates:
27
+ return self.load_template(self.templates[database_type])
28
+ else:
29
+ raise ValueError(f'Unsupported database type. Supported types are: {", ".join(self.templates.keys())}')
30
+
31
+ def get_config_template_text(self, database_type):
32
+ """Retrieve the configuration template as raw text based on the database type."""
33
+ if database_type in self.templates:
34
+ template_filename = self.templates[database_type]
35
+ package_path = f"{self.settings_manager.settings['appPackageName']}.common.templates"
36
+ with resources.files(package_path).joinpath(template_filename) as path, \
37
+ resources.as_file(path) as config_file:
38
+ with open(config_file, 'r') as file:
39
+ return file.read()
40
+ else:
41
+ raise ValueError(f'Unsupported database type. Supported types are: {", ".join(self.templates.keys())}')
42
+
43
+ def write_config_template(self, file_path, config_data):
44
+ """Write the configuration template data to a file."""
45
+ file_path.parent.mkdir(parents=True, exist_ok=True)
46
+ with open(file_path, 'w') as f:
47
+ yaml.dump(config_data, f, sort_keys=False, default_flow_style=False, width=1000)
48
+ print(f'New configuration template created: {file_path}')
49
+
50
+ def write_config_template_text(self, file_path, config_text):
51
+ """Write the configuration template text to a file, preserving all formatting."""
52
+ file_path.parent.mkdir(parents=True, exist_ok=True)
53
+ with open(file_path, 'w') as f:
54
+ f.write(config_text)
55
+ print(f'New configuration template created: {file_path}')
56
+
57
+ def test_connection(self, config_file_path):
58
+ """Test database connection using a configuration file."""
59
+
60
+ # Step 1: Determine the database type.
61
+ with open(config_file_path, 'r') as config_file:
62
+ config_data = yaml.safe_load(config_file)
63
+ database_type = next(iter(config_data))
64
+ print(f"{database_type} database config file has been detected...")
65
+
66
+ # Step 2:
67
+ integration_tester = DatabaseIntegrationTest()
68
+
69
+ if database_type == 'postgresql':
70
+ asyncio.run(integration_tester.test_postgresql_connection(config_file_path))
71
+ else:
72
+ print(f"Unsupported database type: {database_type}")
@@ -0,0 +1,34 @@
1
+ import yaml
2
+ from pgmonkey.connections.postgres.postgres_connection_factory import PostgresConnectionFactory
3
+
4
+
5
+ class PGConnectionManager:
6
+ def __init__(self):
7
+ pass
8
+
9
+ async def get_database_connection(self, config_file_path):
10
+ """Establish a database connection using a configuration file."""
11
+ with open(config_file_path, 'r') as f:
12
+ config_data_dictionary = yaml.safe_load(f)
13
+ database_type = next(iter(config_data_dictionary))
14
+
15
+ if database_type == 'postgresql':
16
+ return await self.get_postgresql_connection(config_data_dictionary)
17
+ else:
18
+ raise ValueError(f"Unsupported database type: {database_type}")
19
+
20
+ async def get_postgresql_connection(self, config_data_dictionary):
21
+ """Create and return PostgreSQL connection based on the configuration."""
22
+ factory = PostgresConnectionFactory(config_data_dictionary)
23
+ connection = factory.get_connection()
24
+ #print(connection.__dict__)
25
+
26
+ if config_data_dictionary['postgresql']['connection_type'] in ['normal', 'pool']:
27
+ connection.connect()
28
+ return connection
29
+ elif config_data_dictionary['postgresql']['connection_type'] in ['async', 'async_pool']:
30
+ await connection.connect()
31
+ #print(connection.__dict__)
32
+ return connection
33
+
34
+
@@ -0,0 +1,22 @@
1
+ import yaml
2
+ from pathlib import Path
3
+
4
+
5
+ class SettingsManager:
6
+ def __init__(self, settings_filename='app_settings.yaml'):
7
+ # Path to the default settings file, it's in the package structure.
8
+ self.settings_file = Path(__file__).resolve().parents[2] / 'settings' / settings_filename
9
+ # Load settings from the default settings file from package structure
10
+ self.settings = self.load_initial_settings()
11
+ # Use the package name from the settings
12
+ self.package_name = self.settings.get('appPackageName')
13
+
14
+ def load_initial_settings(self):
15
+ """Load initial settings from app_settings.yaml located in the same directory as this class."""
16
+ if not self.settings_file.exists():
17
+ raise FileNotFoundError(f"Could not find the settings file {self.settings_file}")
18
+ with open(self.settings_file, "r") as file:
19
+ return yaml.safe_load(file)
20
+
21
+ def print_hello_world(self):
22
+ print(f"{self.package_name} says \"Hello World\" - This is a place holder for the settings cli class.")
@@ -0,0 +1,13 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+
4
+ class ToplevelManager:
5
+ @staticmethod
6
+ def get_package_version(package_name: str) -> str:
7
+ """Fetch the package version using the package name."""
8
+ try:
9
+ return version(package_name)
10
+ except PackageNotFoundError:
11
+ return "Package version not found"
12
+
13
+ # You can add more top-level management methods here
@@ -0,0 +1,103 @@
1
+ import yaml
2
+
3
+ class PostgresServerConfigGenerator:
4
+ def __init__(self, yaml_file_path):
5
+ self.yaml_file_path = yaml_file_path
6
+ self.config = self.read_yaml() # Read the configuration file upon instantiation
7
+
8
+ def read_yaml(self):
9
+ """Reads the YAML configuration file."""
10
+ try:
11
+ with open(self.yaml_file_path, 'r') as file:
12
+ return yaml.safe_load(file)
13
+ except FileNotFoundError:
14
+ print(f"Error: File not found - {self.yaml_file_path}")
15
+ except yaml.YAMLError as e:
16
+ print(f"Error reading YAML file: {e}")
17
+ except Exception as e:
18
+ print(f"An error occurred: {e}")
19
+
20
+ def generate_pg_hba_entry(self):
21
+ """Generates entries for pg_hba.conf based on the SSL settings, with headers for each column."""
22
+ host = self.config['postgresql']['connection_settings']['host']
23
+ sslmode = self.config['postgresql']['connection_settings'].get('sslmode', 'prefer')
24
+ entries = []
25
+ header = "TYPE DATABASE USER ADDRESS METHOD OPTIONS"
26
+ entries.append(header)
27
+ if sslmode in ['verify-ca', 'verify-full']:
28
+ clientcert = 'verify-full' if sslmode == 'verify-full' else 'verify-ca'
29
+ ip_subnet = '.'.join(host.split('.')[:-1]) + '.0/24' # Assuming a /24 subnet
30
+ entry = f"hostssl all all {ip_subnet} md5 clientcert={clientcert}"
31
+ entries.append(entry)
32
+ elif sslmode != 'disable':
33
+ ip_subnet = '.'.join(host.split('.')[:-1]) + '.0/24' # Assuming a /24 subnet
34
+ entry = f"host all all {ip_subnet} reject"
35
+ entries.append(entry)
36
+ return entries
37
+
38
+ def generate_postgresql_conf(self):
39
+ """Generates minimal entries for postgresql.conf to ensure compatibility based on connection pooling settings."""
40
+ settings = []
41
+ connection_type = self.config['postgresql']['connection_type']
42
+ if connection_type in ['pool', 'async_pool']:
43
+ pool_settings = self.config['postgresql'].get('pool_settings', {})
44
+ max_size = pool_settings.get('max_size', 100)
45
+ max_connections = int(max_size * 1.1)
46
+ settings.append(f"max_connections = {max_connections}")
47
+ else:
48
+ settings.append("max_connections = 20")
49
+
50
+ # Generate SSL settings
51
+ settings.extend(self.generate_ssl_settings())
52
+ return settings
53
+
54
+ def generate_ssl_settings(self):
55
+ """Generates SSL configuration entries for postgresql.conf based on client settings."""
56
+ ssl_settings = []
57
+ if 'sslmode' in self.config['postgresql']['connection_settings'] and self.config['postgresql']['connection_settings']['sslmode'] != 'disable':
58
+ ssl_settings.append("ssl = on")
59
+ ssl_settings.append(f"ssl_cert_file = 'server.crt'")
60
+ ssl_settings.append(f"ssl_key_file = 'server.key'")
61
+ ssl_settings.append(f"ssl_ca_file = 'ca.crt'")
62
+ return ssl_settings
63
+
64
+ def print_configurations(self):
65
+ """Prints the generated server configurations and provides final instructions if configurations are suggested."""
66
+ if self.config:
67
+ pg_hba_entries = self.generate_pg_hba_entry()
68
+ postgresql_conf_entries = self.generate_postgresql_conf()
69
+ print("1) Database type detected: PostgreSQL\n")
70
+ print("2) Minimal database server settings needed for this config file:\n")
71
+
72
+ # Print pg_hba.conf configurations if they exist
73
+ if len(pg_hba_entries) > 1: # More than just the header
74
+ print(" a) pg_hba.conf:" + "\n")
75
+ print('\n'.join(pg_hba_entries) + "\n")
76
+ else:
77
+ print(" a) No entries needed for pg_hba.conf.\n")
78
+
79
+ # Print postgresql.conf configurations if they exist
80
+ if postgresql_conf_entries:
81
+ print(" b) postgresql.conf:" + "\n")
82
+ print('\n'.join(postgresql_conf_entries))
83
+ else:
84
+ print(" b) No entries needed for postgresql.conf.\n")
85
+
86
+ # Add a newline before final instructions
87
+ print() # This adds the desired newline
88
+
89
+ # Determine which configuration files had suggestions and list them appropriately
90
+ files_to_check = []
91
+ if len(pg_hba_entries) > 1:
92
+ files_to_check.append("pg_hba.conf")
93
+ if postgresql_conf_entries:
94
+ files_to_check.append("postgresql.conf")
95
+
96
+ if files_to_check:
97
+ print(f"Please check the following files on your system and ensure that the appropriate settings are applied: {', '.join(files_to_check)}.")
98
+ print("Ensure that the network ADDRESS matches your network subnet and review all configurations.")
99
+ else:
100
+ print("Configuration data is not available. Please check the file path and contents.")
101
+
102
+
103
+
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.1
2
+ Name: pgmonkey
3
+ Version: 0.0.1
4
+ Summary: A tool to assist with postgresql database connections
5
+ Author-email: Good Boy <pythonic@rexbytes.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 RexBytes
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/RexBytes/pgmonkey
29
+ Project-URL: Bug Tracker, https://github.com/RexBytes/pgmonkey
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Topic :: Database
34
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
35
+ Requires-Python: >=3.12
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: psycopg[binary]>=3.2.1
39
+ Requires-Dist: psycopg_pool>=3.2.2
40
+ Requires-Dist: PyYAML>=6.0.2
41
+
42
+ # pgmonkey
@@ -0,0 +1,38 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cli/__init__.py
5
+ src/cli/cli.py
6
+ src/cli/cli_pg_server_config_subparser.py
7
+ src/cli/cli_pgconfig_subparser.py
8
+ src/cli/cli_settings_subparser.py
9
+ src/cli/cli_toplevel_parser.py
10
+ src/pgmonkey/__init__.py
11
+ src/pgmonkey.egg-info/PKG-INFO
12
+ src/pgmonkey.egg-info/SOURCES.txt
13
+ src/pgmonkey.egg-info/dependency_links.txt
14
+ src/pgmonkey.egg-info/entry_points.txt
15
+ src/pgmonkey.egg-info/requires.txt
16
+ src/pgmonkey.egg-info/top_level.txt
17
+ src/pgmonkey/common/__init__.py
18
+ src/pgmonkey/common/config/__init__.py
19
+ src/pgmonkey/common/utils/__init__.py
20
+ src/pgmonkey/common/utils/pathutils.py
21
+ src/pgmonkey/connections/__init__.py
22
+ src/pgmonkey/connections/base.py
23
+ src/pgmonkey/connections/postgres/__init__.py
24
+ src/pgmonkey/connections/postgres/async_connection.py
25
+ src/pgmonkey/connections/postgres/async_pool_connection.py
26
+ src/pgmonkey/connections/postgres/base_connection.py
27
+ src/pgmonkey/connections/postgres/normal_connection.py
28
+ src/pgmonkey/connections/postgres/pool_connection.py
29
+ src/pgmonkey/connections/postgres/postgres_connection_factory.py
30
+ src/pgmonkey/managers/__init__.py
31
+ src/pgmonkey/managers/pg_server_config_manager.py
32
+ src/pgmonkey/managers/pgconfig_manager.py
33
+ src/pgmonkey/managers/pgconnection_manager.py
34
+ src/pgmonkey/managers/settings_manager.py
35
+ src/pgmonkey/managers/toplevel_manager.py
36
+ src/pgmonkey/serversettings/postgres_server_config_generator.py
37
+ src/tests/__init__.py
38
+ src/tests/database_integration_test.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pgmonkey = cli.cli:main
@@ -0,0 +1,3 @@
1
+ psycopg[binary]>=3.2.1
2
+ psycopg_pool>=3.2.2
3
+ PyYAML>=6.0.2
@@ -0,0 +1,4 @@
1
+ cli
2
+ pgmonkey
3
+ settings
4
+ tests
File without changes
@@ -0,0 +1,26 @@
1
+ from pgmonkey.managers.pgconnection_manager import PGConnectionManager
2
+ import yaml
3
+
4
+
5
+ class DatabaseIntegrationTest:
6
+ def __init__(self):
7
+ self.pgconnection_manager = PGConnectionManager()
8
+
9
+ async def test_postgresql_connection(self, config_file_path):
10
+ try:
11
+ # Retrieve the database connection; assume it's already prepared to be used as an async context manager
12
+ connection = await self.pgconnection_manager.get_database_connection(config_file_path)
13
+
14
+ # Read the configuration file to determine the connection type
15
+ with open(config_file_path, 'r') as config_file:
16
+ config = yaml.safe_load(config_file)
17
+
18
+ if config['postgresql']['connection_type'] in ['normal', 'pool']:
19
+ with connection as sync_connection:
20
+ sync_connection.test_connection()
21
+ elif config['postgresql']['connection_type'] in ['async', 'async_pool']:
22
+ async with connection as async_connection:
23
+ await async_connection.test_connection()
24
+
25
+ except Exception as e:
26
+ print(f"An error occurred while testing the connection: {e}")