ry-pg-utils 1.0.2__py3-none-any.whl
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.
- ry_pg_utils/__init__.py +0 -0
- ry_pg_utils/config.py +44 -0
- ry_pg_utils/connect.py +288 -0
- ry_pg_utils/dynamic_table.py +199 -0
- ry_pg_utils/ipc/__init__.py +0 -0
- ry_pg_utils/ipc/channels.py +14 -0
- ry_pg_utils/notify_trigger.py +346 -0
- ry_pg_utils/parse_args.py +15 -0
- ry_pg_utils/pb_types/__init__.py +0 -0
- ry_pg_utils/pb_types/database_pb2.py +38 -0
- ry_pg_utils/pb_types/database_pb2.pyi +156 -0
- ry_pg_utils/pb_types/py.typed +0 -0
- ry_pg_utils/postgres_info.py +47 -0
- ry_pg_utils/py.typed +0 -0
- ry_pg_utils/updater.py +181 -0
- ry_pg_utils-1.0.2.dist-info/METADATA +473 -0
- ry_pg_utils-1.0.2.dist-info/RECORD +20 -0
- ry_pg_utils-1.0.2.dist-info/WHEEL +5 -0
- ry_pg_utils-1.0.2.dist-info/licenses/LICENSE +21 -0
- ry_pg_utils-1.0.2.dist-info/top_level.txt +1 -0
ry_pg_utils/updater.py
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
import argparse
|
2
|
+
import time
|
3
|
+
import typing as T
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
from ry_redis_bus.helpers import RedisInfo, message_handler
|
7
|
+
from ry_redis_bus.redis_client_base import RedisClientBase
|
8
|
+
from ryutils import log
|
9
|
+
from ryutils.verbose import Verbose
|
10
|
+
|
11
|
+
from ry_pg_utils.connect import close_engine, init_database, is_database_initialized
|
12
|
+
from ry_pg_utils.ipc import channels
|
13
|
+
from ry_pg_utils.pb_types.database_pb2 import ( # pylint: disable=no-name-in-module
|
14
|
+
DatabaseSettingsPb,
|
15
|
+
PostgresMessagePb,
|
16
|
+
PostgresPb,
|
17
|
+
)
|
18
|
+
from ry_pg_utils.postgres_info import PostgresInfo
|
19
|
+
|
20
|
+
DB_RETRY_TIME = 5.0
|
21
|
+
|
22
|
+
|
23
|
+
def get_database_settings(
|
24
|
+
message_pb: DatabaseSettingsPb, backend_id: str, verbose: bool = False
|
25
|
+
) -> T.Optional[PostgresInfo]:
|
26
|
+
raw_db_config_pb = message_pb.postgres.get(backend_id, None)
|
27
|
+
|
28
|
+
if raw_db_config_pb is None:
|
29
|
+
return None
|
30
|
+
|
31
|
+
if not message_pb.primaryDatabase:
|
32
|
+
return None
|
33
|
+
|
34
|
+
db_config_pb: PostgresPb = raw_db_config_pb
|
35
|
+
|
36
|
+
if verbose:
|
37
|
+
log.print_normal(f"Received database settings {backend_id}:\n{db_config_pb}")
|
38
|
+
|
39
|
+
database_settings_msg = PostgresInfo(
|
40
|
+
db_name=db_config_pb.database,
|
41
|
+
user=db_config_pb.user,
|
42
|
+
password=db_config_pb.password,
|
43
|
+
host=db_config_pb.host,
|
44
|
+
port=db_config_pb.port,
|
45
|
+
)
|
46
|
+
|
47
|
+
return database_settings_msg
|
48
|
+
|
49
|
+
|
50
|
+
@dataclass
|
51
|
+
class DbUpdater(RedisClientBase):
|
52
|
+
do_publish_db: bool
|
53
|
+
postgres_info: PostgresInfo
|
54
|
+
backend_id: str
|
55
|
+
logging_error_db_callback: T.Callable[[str, str], None] | None
|
56
|
+
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
redis_info: RedisInfo,
|
60
|
+
args: argparse.Namespace,
|
61
|
+
backend_id: str,
|
62
|
+
verbose: Verbose,
|
63
|
+
logging_error_db_callback: T.Callable[[str, str], None] | None = None,
|
64
|
+
):
|
65
|
+
super().__init__(
|
66
|
+
redis_info=redis_info,
|
67
|
+
verbose=verbose,
|
68
|
+
)
|
69
|
+
|
70
|
+
self.do_publish_db = args.do_publish_db if args else True
|
71
|
+
|
72
|
+
self.postgres_info = PostgresInfo(
|
73
|
+
db_name=args.postgres_db,
|
74
|
+
user=args.postgres_user,
|
75
|
+
password=args.postgres_password,
|
76
|
+
host=args.postgres_host,
|
77
|
+
port=args.postgres_port,
|
78
|
+
)
|
79
|
+
|
80
|
+
self.last_db_init_retry_time = 0.0
|
81
|
+
self.use_local_db_only = args.use_local_db_only if args else True
|
82
|
+
self.database_settings_msg: T.Optional[PostgresInfo] = None
|
83
|
+
self.backend_id = backend_id
|
84
|
+
self.logging_error_db_callback = logging_error_db_callback
|
85
|
+
|
86
|
+
@message_handler
|
87
|
+
def handle_database_config_message(self, message_pb: DatabaseSettingsPb) -> None:
|
88
|
+
database_settings = get_database_settings(message_pb, backend_id=self.backend_id)
|
89
|
+
|
90
|
+
if database_settings is not None:
|
91
|
+
self.database_settings_msg = database_settings
|
92
|
+
|
93
|
+
def init(self) -> None:
|
94
|
+
super().start()
|
95
|
+
|
96
|
+
if self.use_local_db_only:
|
97
|
+
init_database(
|
98
|
+
db_host=self.postgres_info.host,
|
99
|
+
db_port=self.postgres_info.port,
|
100
|
+
db_name=self.postgres_info.db_name,
|
101
|
+
db_user=self.postgres_info.user,
|
102
|
+
db_password=self.postgres_info.password,
|
103
|
+
)
|
104
|
+
else:
|
105
|
+
self.postgres_info = PostgresInfo.null()
|
106
|
+
log.print_fail(f"DbUpdater initialized with null database info: {self.postgres_info}")
|
107
|
+
|
108
|
+
self.subscribe(channels.DATABASE_CONFIG_CHANNEL, self.handle_database_config_message)
|
109
|
+
|
110
|
+
def update_db(self, postgres_info: PostgresInfo) -> None:
|
111
|
+
self.postgres_info = postgres_info
|
112
|
+
|
113
|
+
def get_info(self) -> T.Optional[PostgresInfo]:
|
114
|
+
return self.postgres_info
|
115
|
+
|
116
|
+
def step(self, force: bool = False) -> None:
|
117
|
+
now = time.time()
|
118
|
+
|
119
|
+
super().step()
|
120
|
+
|
121
|
+
if not force and now - self.last_db_init_retry_time < DB_RETRY_TIME:
|
122
|
+
return
|
123
|
+
|
124
|
+
self.last_db_init_retry_time = now
|
125
|
+
|
126
|
+
self.maybe_update_database()
|
127
|
+
|
128
|
+
def maybe_update_database(self) -> None:
|
129
|
+
new_postgres_info: T.Optional[PostgresInfo] = (
|
130
|
+
self.postgres_info if self.database_settings_msg is None else self.database_settings_msg
|
131
|
+
)
|
132
|
+
|
133
|
+
self.database_settings_msg = None
|
134
|
+
|
135
|
+
if new_postgres_info is None:
|
136
|
+
return
|
137
|
+
|
138
|
+
if new_postgres_info.is_null():
|
139
|
+
log.print_fail(f"Database info is null: {new_postgres_info}")
|
140
|
+
return
|
141
|
+
|
142
|
+
if new_postgres_info == self.postgres_info and is_database_initialized(
|
143
|
+
new_postgres_info.db_name
|
144
|
+
):
|
145
|
+
return
|
146
|
+
|
147
|
+
log.print_bold(f"Updating database to {new_postgres_info}")
|
148
|
+
|
149
|
+
close_engine(self.postgres_info.db_name)
|
150
|
+
init_database(
|
151
|
+
db_host=new_postgres_info.host,
|
152
|
+
db_port=new_postgres_info.port,
|
153
|
+
db_name=new_postgres_info.db_name,
|
154
|
+
db_user=new_postgres_info.user,
|
155
|
+
db_password=new_postgres_info.password,
|
156
|
+
)
|
157
|
+
|
158
|
+
self.postgres_info = new_postgres_info
|
159
|
+
|
160
|
+
db_message_pb = PostgresMessagePb()
|
161
|
+
postgres_db_pb = PostgresPb()
|
162
|
+
postgres_db_pb.database = self.postgres_info.db_name
|
163
|
+
postgres_db_pb.user = self.postgres_info.user
|
164
|
+
postgres_db_pb.password = self.postgres_info.password
|
165
|
+
postgres_db_pb.host = self.postgres_info.host
|
166
|
+
postgres_db_pb.port = self.postgres_info.port
|
167
|
+
db_message_pb.postgres.CopyFrom(postgres_db_pb)
|
168
|
+
db_message_pb.utime.GetCurrentTime()
|
169
|
+
if self.do_publish_db:
|
170
|
+
self.publish(channels.DATABASE_CHANNEL, db_message_pb.SerializeToString())
|
171
|
+
|
172
|
+
def log_print_callback(message: str) -> None:
|
173
|
+
if self.logging_error_db_callback:
|
174
|
+
self.logging_error_db_callback(message, self.postgres_info.db_name)
|
175
|
+
|
176
|
+
log.update_callback(log_print_callback)
|
177
|
+
|
178
|
+
def run(self) -> None:
|
179
|
+
while True:
|
180
|
+
self.step()
|
181
|
+
time.sleep(1)
|
@@ -0,0 +1,473 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: ry-pg-utils
|
3
|
+
Version: 1.0.2
|
4
|
+
Summary: Utility functions for PostgreSQL
|
5
|
+
Author: Ross Yeager
|
6
|
+
Author-email: ryeager12@email.com
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
8
|
+
Classifier: Intended Audience :: Developers
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENSE
|
14
|
+
Requires-Dist: ry_redis_bus
|
15
|
+
Requires-Dist: ryutils
|
16
|
+
Requires-Dist: sqlalchemy
|
17
|
+
Requires-Dist: sqlalchemy_utils
|
18
|
+
Requires-Dist: psycopg2-binary
|
19
|
+
Requires-Dist: python-dotenv
|
20
|
+
Requires-Dist: tenacity
|
21
|
+
Requires-Dist: types-protobuf
|
22
|
+
Requires-Dist: types-psycopg2
|
23
|
+
Dynamic: author
|
24
|
+
Dynamic: author-email
|
25
|
+
Dynamic: classifier
|
26
|
+
Dynamic: description
|
27
|
+
Dynamic: description-content-type
|
28
|
+
Dynamic: license-file
|
29
|
+
Dynamic: requires-dist
|
30
|
+
Dynamic: summary
|
31
|
+
|
32
|
+
# ry-pg-utils
|
33
|
+
|
34
|
+
A Python utility library for PostgreSQL database operations with dynamic table creation, connection management, and Protocol Buffer integration.
|
35
|
+
|
36
|
+
## Overview
|
37
|
+
|
38
|
+
`ry-pg-utils` provides a robust framework for working with PostgreSQL databases in Python applications. It includes utilities for:
|
39
|
+
|
40
|
+
- Database connection management with connection pooling
|
41
|
+
- Dynamic table creation from Protocol Buffer message definitions
|
42
|
+
- Thread-safe session management
|
43
|
+
- Multi-backend support with automatic backend ID tracking
|
44
|
+
- Database updater for dynamic configuration
|
45
|
+
- Argument parsing for PostgreSQL connection parameters
|
46
|
+
|
47
|
+
## Features
|
48
|
+
|
49
|
+
- **Connection Management**: Thread-safe PostgreSQL connection pooling with automatic retry logic
|
50
|
+
- **Dynamic Tables**: Automatically create and manage database tables from Protocol Buffer message schemas
|
51
|
+
- **Multi-Backend Support**: Track data across multiple backend instances with automatic ID tagging
|
52
|
+
- **Session Management**: Context managers for safe database session handling
|
53
|
+
- **Configuration System**: Flexible configuration via environment variables and runtime settings
|
54
|
+
- **Type Safety**: Full type hints and mypy support
|
55
|
+
|
56
|
+
## Installation
|
57
|
+
|
58
|
+
```bash
|
59
|
+
pip install ry-pg-utils
|
60
|
+
```
|
61
|
+
|
62
|
+
### Dependencies
|
63
|
+
|
64
|
+
- Python 3.12+
|
65
|
+
- PostgreSQL database
|
66
|
+
- SQLAlchemy
|
67
|
+
- Protocol Buffer support
|
68
|
+
|
69
|
+
## Configuration
|
70
|
+
|
71
|
+
`ry-pg-utils` uses a flexible configuration system that can be customized in multiple ways:
|
72
|
+
|
73
|
+
### 1. Environment Variables
|
74
|
+
|
75
|
+
Create a `.env` file in your project root:
|
76
|
+
|
77
|
+
```bash
|
78
|
+
POSTGRES_HOST=localhost
|
79
|
+
POSTGRES_PORT=5432
|
80
|
+
POSTGRES_DB=mydb
|
81
|
+
POSTGRES_USER=postgres
|
82
|
+
POSTGRES_PASSWORD=secret
|
83
|
+
```
|
84
|
+
|
85
|
+
### 2. Configuration Object
|
86
|
+
|
87
|
+
```python
|
88
|
+
from ry_pg_utils.config import pg_config
|
89
|
+
|
90
|
+
# Access configuration
|
91
|
+
print(pg_config.postgres_host)
|
92
|
+
print(pg_config.postgres_port)
|
93
|
+
|
94
|
+
# Modify at runtime
|
95
|
+
pg_config.add_backend_to_all = False
|
96
|
+
pg_config.backend_id = "custom_backend_id"
|
97
|
+
```
|
98
|
+
|
99
|
+
### Configuration Options
|
100
|
+
|
101
|
+
| Option | Type | Default | Description |
|
102
|
+
|--------|------|---------|-------------|
|
103
|
+
| `postgres_host` | str | From env | PostgreSQL server hostname |
|
104
|
+
| `postgres_port` | int | From env | PostgreSQL server port |
|
105
|
+
| `postgres_db` | str | From env | Database name |
|
106
|
+
| `postgres_user` | str | From env | Database username |
|
107
|
+
| `postgres_password` | str | From env | Database password |
|
108
|
+
| `backend_id` | str | hostname_ip | Unique identifier for this backend instance |
|
109
|
+
| `add_backend_to_all` | bool | True | Add backend_id column to all tables |
|
110
|
+
| `add_backend_to_tables` | bool | True | Append backend_id to table names |
|
111
|
+
| `raise_on_use_before_init` | bool | True | Raise exception if DB used before initialization |
|
112
|
+
| `do_publish_db` | bool | False | Enable database publishing features |
|
113
|
+
| `use_local_db_only` | bool | True | Use only local database connections |
|
114
|
+
|
115
|
+
## Quick Start
|
116
|
+
|
117
|
+
### 1. Initialize Database Connection
|
118
|
+
|
119
|
+
```python
|
120
|
+
from ry_pg_utils.connect import init_database, ManagedSession
|
121
|
+
from ry_pg_utils.postgres_info import PostgresInfo
|
122
|
+
|
123
|
+
# Create database connection info
|
124
|
+
db_info = PostgresInfo(
|
125
|
+
db_name="myapp_db",
|
126
|
+
host="localhost",
|
127
|
+
port=5432,
|
128
|
+
user="postgres",
|
129
|
+
password="secret"
|
130
|
+
)
|
131
|
+
|
132
|
+
# Initialize the database connection
|
133
|
+
init_database(db_info, db_name="myapp")
|
134
|
+
```
|
135
|
+
|
136
|
+
### 2. Use Dynamic Tables with Protocol Buffers
|
137
|
+
|
138
|
+
```python
|
139
|
+
from ry_pg_utils.dynamic_table import DynamicTableDb
|
140
|
+
from your_app.proto import YourMessagePb
|
141
|
+
|
142
|
+
# Create a message
|
143
|
+
message = YourMessagePb()
|
144
|
+
message.field1 = "value1"
|
145
|
+
message.field2 = 42
|
146
|
+
|
147
|
+
# Log message to database (table created automatically)
|
148
|
+
DynamicTableDb.log_data_to_db(
|
149
|
+
msg=message,
|
150
|
+
db_name="myapp",
|
151
|
+
channel="my_channel"
|
152
|
+
)
|
153
|
+
|
154
|
+
# Check if data exists
|
155
|
+
exists = DynamicTableDb.is_in_db(
|
156
|
+
msg=message,
|
157
|
+
db_name="myapp",
|
158
|
+
channel="my_channel",
|
159
|
+
attr="field1",
|
160
|
+
value="value1"
|
161
|
+
)
|
162
|
+
```
|
163
|
+
|
164
|
+
### 3. Manual Session Management
|
165
|
+
|
166
|
+
```python
|
167
|
+
from ry_pg_utils.connect import ManagedSession
|
168
|
+
|
169
|
+
# Use context manager for automatic session cleanup
|
170
|
+
with ManagedSession(db="myapp") as session:
|
171
|
+
result = session.execute("SELECT * FROM my_table")
|
172
|
+
for row in result:
|
173
|
+
print(row)
|
174
|
+
```
|
175
|
+
|
176
|
+
## Core Components
|
177
|
+
|
178
|
+
### `connect.py` - Connection Management
|
179
|
+
|
180
|
+
The connection module provides thread-safe database connection and session management:
|
181
|
+
|
182
|
+
```python
|
183
|
+
from ry_pg_utils.connect import (
|
184
|
+
init_database, # Initialize database connection
|
185
|
+
init_engine, # Initialize SQLAlchemy engine
|
186
|
+
ManagedSession, # Context manager for sessions
|
187
|
+
get_backend_id, # Get current backend ID
|
188
|
+
set_backend_id, # Set backend ID for thread
|
189
|
+
close_engine, # Close database connection
|
190
|
+
clear_db, # Clear all connections
|
191
|
+
)
|
192
|
+
```
|
193
|
+
|
194
|
+
**Key Features:**
|
195
|
+
- Thread-local backend ID tracking
|
196
|
+
- Connection pooling with configurable parameters
|
197
|
+
- Automatic connection recovery on failure
|
198
|
+
- Session scoping for thread safety
|
199
|
+
|
200
|
+
### `dynamic_table.py` - Dynamic Table Creation
|
201
|
+
|
202
|
+
Automatically create and manage database tables from Protocol Buffer definitions:
|
203
|
+
|
204
|
+
```python
|
205
|
+
from ry_pg_utils.dynamic_table import DynamicTableDb
|
206
|
+
|
207
|
+
# Create instance
|
208
|
+
db = DynamicTableDb(db_name="myapp")
|
209
|
+
|
210
|
+
# Add message to database
|
211
|
+
db.add_message(
|
212
|
+
channel_name="events",
|
213
|
+
message_pb=my_protobuf_message,
|
214
|
+
log_print_failure=True,
|
215
|
+
verbose=True
|
216
|
+
)
|
217
|
+
|
218
|
+
# Check existence
|
219
|
+
exists = db.inst_is_in_db(
|
220
|
+
message_pb=my_protobuf_message,
|
221
|
+
channel_name="events",
|
222
|
+
attr="event_id",
|
223
|
+
value=12345
|
224
|
+
)
|
225
|
+
```
|
226
|
+
|
227
|
+
**Supported Protocol Buffer Types:**
|
228
|
+
- `int32`, `int64`, `uint32`, `uint64` → PostgreSQL `Integer`
|
229
|
+
- `float`, `double` → PostgreSQL `Float`
|
230
|
+
- `bool` → PostgreSQL `Boolean`
|
231
|
+
- `string` → PostgreSQL `String`
|
232
|
+
- `bytes` → PostgreSQL `LargeBinary`
|
233
|
+
- `Timestamp` (message) → PostgreSQL `DateTime`
|
234
|
+
|
235
|
+
### `postgres_info.py` - Connection Information
|
236
|
+
|
237
|
+
Data class for PostgreSQL connection parameters:
|
238
|
+
|
239
|
+
```python
|
240
|
+
from ry_pg_utils.postgres_info import PostgresInfo
|
241
|
+
|
242
|
+
db_info = PostgresInfo(
|
243
|
+
db_name="mydb",
|
244
|
+
host="localhost",
|
245
|
+
port=5432,
|
246
|
+
user="postgres",
|
247
|
+
password="secret"
|
248
|
+
)
|
249
|
+
|
250
|
+
# Get connection URI
|
251
|
+
uri = db_info.get_uri() # postgresql://postgres:secret@localhost:5432/mydb
|
252
|
+
```
|
253
|
+
|
254
|
+
### `parse_args.py` - Argument Parsing
|
255
|
+
|
256
|
+
Add PostgreSQL arguments to your argument parser:
|
257
|
+
|
258
|
+
```python
|
259
|
+
import argparse
|
260
|
+
from ry_pg_utils.parse_args import add_postrgres_db_args
|
261
|
+
|
262
|
+
parser = argparse.ArgumentParser()
|
263
|
+
add_postrgres_db_args(parser)
|
264
|
+
|
265
|
+
args = parser.parse_args()
|
266
|
+
# Access: args.postgres_host, args.postgres_port, etc.
|
267
|
+
```
|
268
|
+
|
269
|
+
### `updater.py` - Database Configuration Updater
|
270
|
+
|
271
|
+
Dynamically update database connections based on configuration messages:
|
272
|
+
|
273
|
+
```python
|
274
|
+
from ry_pg_utils.updater import PostgresDbUpdater
|
275
|
+
|
276
|
+
updater = PostgresDbUpdater(
|
277
|
+
redis_info=redis_info,
|
278
|
+
verbose=verbose,
|
279
|
+
backend_id="my_backend"
|
280
|
+
)
|
281
|
+
|
282
|
+
# Start listening for configuration updates
|
283
|
+
updater.run()
|
284
|
+
```
|
285
|
+
|
286
|
+
## Advanced Usage
|
287
|
+
|
288
|
+
### Multi-Backend Support
|
289
|
+
|
290
|
+
When `add_backend_to_all` is enabled, all tables automatically get a `backend_id` column:
|
291
|
+
|
292
|
+
```python
|
293
|
+
from ry_pg_utils.connect import set_backend_id, ManagedSession
|
294
|
+
|
295
|
+
# Set backend ID for current thread
|
296
|
+
set_backend_id("backend_1")
|
297
|
+
|
298
|
+
# All subsequent operations will include this backend_id
|
299
|
+
with ManagedSession(db="myapp") as session:
|
300
|
+
# Queries automatically filter by backend_id
|
301
|
+
result = session.execute("SELECT * FROM my_table")
|
302
|
+
```
|
303
|
+
|
304
|
+
### Custom Table Names
|
305
|
+
|
306
|
+
When `add_backend_to_tables` is enabled, table names are automatically suffixed:
|
307
|
+
|
308
|
+
```python
|
309
|
+
from ry_pg_utils.connect import get_table_name
|
310
|
+
|
311
|
+
# Returns "events_my_backend" if add_backend_to_tables=True
|
312
|
+
table_name = get_table_name("events", backend_id="my_backend")
|
313
|
+
```
|
314
|
+
|
315
|
+
### ORM Base Class
|
316
|
+
|
317
|
+
Use the pre-configured base class for SQLAlchemy models:
|
318
|
+
|
319
|
+
```python
|
320
|
+
from ry_pg_utils.connect import Base
|
321
|
+
from sqlalchemy import Column, Integer, String
|
322
|
+
|
323
|
+
class User(Base):
|
324
|
+
__tablename__ = 'users'
|
325
|
+
|
326
|
+
id = Column(Integer, primary_key=True)
|
327
|
+
name = Column(String(100))
|
328
|
+
email = Column(String(200))
|
329
|
+
|
330
|
+
# If add_backend_to_all=True, backend_id column is automatically added
|
331
|
+
```
|
332
|
+
|
333
|
+
## Error Handling
|
334
|
+
|
335
|
+
The library includes robust error handling:
|
336
|
+
|
337
|
+
```python
|
338
|
+
from ry_pg_utils.connect import ManagedSession
|
339
|
+
|
340
|
+
with ManagedSession(db="myapp") as session:
|
341
|
+
if session is None:
|
342
|
+
# Connection failed, handle gracefully
|
343
|
+
print("Failed to establish database connection")
|
344
|
+
return
|
345
|
+
|
346
|
+
try:
|
347
|
+
session.execute("SELECT * FROM my_table")
|
348
|
+
except Exception as e:
|
349
|
+
# Session will automatically rollback
|
350
|
+
print(f"Query failed: {e}")
|
351
|
+
```
|
352
|
+
|
353
|
+
## Type Safety
|
354
|
+
|
355
|
+
The library is fully typed and includes a `py.typed` marker for mypy support:
|
356
|
+
|
357
|
+
```bash
|
358
|
+
# Run type checking
|
359
|
+
mypy your_app.py
|
360
|
+
```
|
361
|
+
|
362
|
+
## Development
|
363
|
+
|
364
|
+
### Setup Development Environment
|
365
|
+
|
366
|
+
```bash
|
367
|
+
# Clone the repository
|
368
|
+
git clone https://github.com/yourusername/ry-pg-utils.git
|
369
|
+
cd ry-pg-utils
|
370
|
+
|
371
|
+
# Create virtual environment
|
372
|
+
python -m venv venv-dev
|
373
|
+
source venv-dev/bin/activate # On Windows: venv-dev\Scripts\activate
|
374
|
+
|
375
|
+
# Install dependencies
|
376
|
+
pip install -r packages/requirements-dev.txt
|
377
|
+
```
|
378
|
+
|
379
|
+
### Running Tests
|
380
|
+
|
381
|
+
```bash
|
382
|
+
# Activate virtual environment
|
383
|
+
source venv-dev/bin/activate
|
384
|
+
|
385
|
+
# Run tests
|
386
|
+
python -m pytest test/
|
387
|
+
```
|
388
|
+
|
389
|
+
### Code Quality
|
390
|
+
|
391
|
+
The project uses several tools for code quality:
|
392
|
+
|
393
|
+
```bash
|
394
|
+
# Format code
|
395
|
+
black ry_pg_utils/
|
396
|
+
|
397
|
+
# Type checking
|
398
|
+
mypy ry_pg_utils/
|
399
|
+
|
400
|
+
# Linting
|
401
|
+
pylint ry_pg_utils/
|
402
|
+
|
403
|
+
# Import sorting
|
404
|
+
isort ry_pg_utils/
|
405
|
+
```
|
406
|
+
|
407
|
+
## Examples
|
408
|
+
|
409
|
+
### Complete Application Example
|
410
|
+
|
411
|
+
```python
|
412
|
+
import argparse
|
413
|
+
from ry_pg_utils.parse_args import add_postrgres_db_args
|
414
|
+
from ry_pg_utils.connect import init_database, ManagedSession
|
415
|
+
from ry_pg_utils.postgres_info import PostgresInfo
|
416
|
+
from ry_pg_utils.dynamic_table import DynamicTableDb
|
417
|
+
|
418
|
+
def parse_args():
|
419
|
+
parser = argparse.ArgumentParser(description="My Database App")
|
420
|
+
add_postrgres_db_args(parser)
|
421
|
+
return parser.parse_args()
|
422
|
+
|
423
|
+
def main():
|
424
|
+
args = parse_args()
|
425
|
+
|
426
|
+
# Initialize database
|
427
|
+
db_info = PostgresInfo(
|
428
|
+
db_name=args.postgres_db,
|
429
|
+
host=args.postgres_host,
|
430
|
+
port=args.postgres_port,
|
431
|
+
user=args.postgres_user,
|
432
|
+
password=args.postgres_password
|
433
|
+
)
|
434
|
+
|
435
|
+
init_database(db_info, db_name="myapp")
|
436
|
+
|
437
|
+
# Use the database
|
438
|
+
with ManagedSession(db="myapp") as session:
|
439
|
+
if session:
|
440
|
+
result = session.execute("SELECT version()")
|
441
|
+
print(f"PostgreSQL version: {result.fetchone()[0]}")
|
442
|
+
|
443
|
+
if __name__ == "__main__":
|
444
|
+
main()
|
445
|
+
```
|
446
|
+
|
447
|
+
## License
|
448
|
+
|
449
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
450
|
+
|
451
|
+
## Contributing
|
452
|
+
|
453
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
454
|
+
|
455
|
+
1. Fork the repository
|
456
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
457
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
458
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
459
|
+
5. Open a Pull Request
|
460
|
+
|
461
|
+
## Author
|
462
|
+
|
463
|
+
Ross Yeager - ryeager12@email.com
|
464
|
+
|
465
|
+
## Changelog
|
466
|
+
|
467
|
+
### Version 1.0.0
|
468
|
+
- Initial release
|
469
|
+
- Database connection management
|
470
|
+
- Dynamic table creation
|
471
|
+
- Multi-backend support
|
472
|
+
- Configuration system
|
473
|
+
- Protocol Buffer integration
|
@@ -0,0 +1,20 @@
|
|
1
|
+
ry_pg_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
ry_pg_utils/config.py,sha256=eR61lRJo4O7N_p4IUwcSgHVf9kdXbaDobScD9Gh-MSA,1169
|
3
|
+
ry_pg_utils/connect.py,sha256=jF-Sj9oySPDfTlOh4_C6c03t4KW5Sef_MLHx_sh52a4,10108
|
4
|
+
ry_pg_utils/dynamic_table.py,sha256=VBkfbATRk_pW3EtAFHvMkGG0fLZMebw_LdWkFaDDugw,6866
|
5
|
+
ry_pg_utils/notify_trigger.py,sha256=_dsuPkoqjnSQqWrsdZK3FqI6zG5UuygVXHwtERwN99Y,12069
|
6
|
+
ry_pg_utils/parse_args.py,sha256=K5nzSfRhIBTazCqSi_FCCjLVdI_5a7G8MtqMwuGCjO4,752
|
7
|
+
ry_pg_utils/postgres_info.py,sha256=mj9er830jvrJXUFuFKx1EmZMjuEpMDcd901kgYb_Cck,1495
|
8
|
+
ry_pg_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
ry_pg_utils/updater.py,sha256=TbUKJHQarzRrDjcbjbzIhy1sO61o-YhEG594gWnI0LA,5831
|
10
|
+
ry_pg_utils/ipc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
ry_pg_utils/ipc/channels.py,sha256=Qn1SUcxkl_vnM-ZejZalVRVu4sGPuNIWIbxmo8Q0DVI,612
|
12
|
+
ry_pg_utils/pb_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
ry_pg_utils/pb_types/database_pb2.py,sha256=yf__HycGrVoU8pzhYdnOhXhfAkgjeGl80ls_RsLemy4,2780
|
14
|
+
ry_pg_utils/pb_types/database_pb2.pyi,sha256=svpmlEY_99dA_u3CJqn7CO5D7CH2pAGTOqlhVMZLXC4,5689
|
15
|
+
ry_pg_utils/pb_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
ry_pg_utils-1.0.2.dist-info/licenses/LICENSE,sha256=PYnig94SABlde939TWrqvOqQkkfjuHttf8KpWKlPFlA,1068
|
17
|
+
ry_pg_utils-1.0.2.dist-info/METADATA,sha256=VGFQxqR7N686IzcfhxIJNFy0WZnPolV2DyzrGQESu8Q,11858
|
18
|
+
ry_pg_utils-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
19
|
+
ry_pg_utils-1.0.2.dist-info/top_level.txt,sha256=OruzbmsQHYyPnAw8RichQ5GK1Sxj-MQwWfJ93PEHXIM,12
|
20
|
+
ry_pg_utils-1.0.2.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Ross Yeager
|
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 @@
|
|
1
|
+
ry_pg_utils
|