ry-pg-utils 1.0.2__py3-none-any.whl → 1.0.3__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.
@@ -3,8 +3,6 @@ from ry_redis_bus.channels import Channel
3
3
  from ry_pg_utils.pb_types.database_pb2 import DatabaseConfigPb # pylint: disable=no-name-in-module
4
4
  from ry_pg_utils.pb_types.database_pb2 import ( # pylint: disable=no-name-in-module
5
5
  DatabaseNotificationPb,
6
- )
7
- from ry_pg_utils.pb_types.database_pb2 import ( # pylint: disable=no-name-in-module
8
6
  DatabaseSettingsPb,
9
7
  )
10
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ry-pg-utils
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Utility functions for PostgreSQL
5
5
  Author: Ross Yeager
6
6
  Author-email: ryeager12@email.com
@@ -31,7 +31,7 @@ Dynamic: summary
31
31
 
32
32
  # ry-pg-utils
33
33
 
34
- A Python utility library for PostgreSQL database operations with dynamic table creation, connection management, and Protocol Buffer integration.
34
+ A Python utility library for PostgreSQL database operations with dynamic table creation, connection management, Protocol Buffer integration, and PostgreSQL LISTEN/NOTIFY support.
35
35
 
36
36
  ## Overview
37
37
 
@@ -41,16 +41,19 @@ A Python utility library for PostgreSQL database operations with dynamic table c
41
41
  - Dynamic table creation from Protocol Buffer message definitions
42
42
  - Thread-safe session management
43
43
  - Multi-backend support with automatic backend ID tracking
44
- - Database updater for dynamic configuration
44
+ - PostgreSQL LISTEN/NOTIFY triggers and notifications
45
+ - Database updater for dynamic configuration via Redis
45
46
  - Argument parsing for PostgreSQL connection parameters
46
47
 
47
48
  ## Features
48
49
 
49
- - **Connection Management**: Thread-safe PostgreSQL connection pooling with automatic retry logic
50
+ - **Connection Management**: Thread-safe PostgreSQL connection pooling with automatic retry logic and health checks
50
51
  - **Dynamic Tables**: Automatically create and manage database tables from Protocol Buffer message schemas
51
52
  - **Multi-Backend Support**: Track data across multiple backend instances with automatic ID tagging
52
53
  - **Session Management**: Context managers for safe database session handling
54
+ - **Notification System**: Built-in PostgreSQL LISTEN/NOTIFY support with triggers and callbacks
53
55
  - **Configuration System**: Flexible configuration via environment variables and runtime settings
56
+ - **Redis Integration**: Optional Redis-based database configuration updates
54
57
  - **Type Safety**: Full type hints and mypy support
55
58
 
56
59
  ## Installation
@@ -63,8 +66,13 @@ pip install ry-pg-utils
63
66
 
64
67
  - Python 3.12+
65
68
  - PostgreSQL database
66
- - SQLAlchemy
67
- - Protocol Buffer support
69
+ - SQLAlchemy 2.0+
70
+ - Protocol Buffer support (protobuf)
71
+ - psycopg2-binary
72
+ - tenacity (for retry logic)
73
+ - python-dotenv (for environment variables)
74
+ - ryutils (logging utilities)
75
+ - ry_redis_bus (optional, for Redis integration)
68
76
 
69
77
  ## Configuration
70
78
 
@@ -105,11 +113,11 @@ pg_config.backend_id = "custom_backend_id"
105
113
  | `postgres_db` | str | From env | Database name |
106
114
  | `postgres_user` | str | From env | Database username |
107
115
  | `postgres_password` | str | From env | Database password |
108
- | `backend_id` | str | hostname_ip | Unique identifier for this backend instance |
116
+ | `backend_id` | str | POSTGRES_USER or hostname_ip | Unique identifier for this backend instance |
109
117
  | `add_backend_to_all` | bool | True | Add backend_id column to all tables |
110
118
  | `add_backend_to_tables` | bool | True | Append backend_id to table names |
111
119
  | `raise_on_use_before_init` | bool | True | Raise exception if DB used before initialization |
112
- | `do_publish_db` | bool | False | Enable database publishing features |
120
+ | `do_publish_db` | bool | False | Enable database publishing features (for Redis integration) |
113
121
  | `use_local_db_only` | bool | True | Use only local database connections |
114
122
 
115
123
  ## Quick Start
@@ -118,19 +126,15 @@ pg_config.backend_id = "custom_backend_id"
118
126
 
119
127
  ```python
120
128
  from ry_pg_utils.connect import init_database, ManagedSession
121
- from ry_pg_utils.postgres_info import PostgresInfo
122
129
 
123
- # Create database connection info
124
- db_info = PostgresInfo(
130
+ # Initialize the database connection
131
+ init_database(
125
132
  db_name="myapp_db",
126
- host="localhost",
127
- port=5432,
128
- user="postgres",
129
- password="secret"
133
+ db_host="localhost",
134
+ db_port=5432,
135
+ db_user="postgres",
136
+ db_password="secret"
130
137
  )
131
-
132
- # Initialize the database connection
133
- init_database(db_info, db_name="myapp")
134
138
  ```
135
139
 
136
140
  ### 2. Use Dynamic Tables with Protocol Buffers
@@ -165,12 +169,14 @@ exists = DynamicTableDb.is_in_db(
165
169
 
166
170
  ```python
167
171
  from ry_pg_utils.connect import ManagedSession
172
+ from sqlalchemy import text
168
173
 
169
174
  # 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)
175
+ with ManagedSession(db="myapp_db") as session:
176
+ if session:
177
+ result = session.execute(text("SELECT * FROM my_table"))
178
+ for row in result:
179
+ print(row)
174
180
  ```
175
181
 
176
182
  ## Core Components
@@ -181,21 +187,28 @@ The connection module provides thread-safe database connection and session manag
181
187
 
182
188
  ```python
183
189
  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
190
+ init_database, # Initialize database connection
191
+ init_engine, # Initialize SQLAlchemy engine
192
+ ManagedSession, # Context manager for sessions
193
+ get_backend_id, # Get current backend ID
194
+ set_backend_id, # Set backend ID for thread
195
+ get_engine, # Get engine for database
196
+ close_engine, # Close database connection
197
+ clear_db, # Clear all connections
198
+ get_table_name, # Get table name with backend suffix
199
+ is_database_initialized, # Check if database is initialized
200
+ Base, # SQLAlchemy declarative base
191
201
  )
192
202
  ```
193
203
 
194
204
  **Key Features:**
195
205
  - Thread-local backend ID tracking
196
- - Connection pooling with configurable parameters
197
- - Automatic connection recovery on failure
206
+ - Connection pooling with configurable parameters (pool_size, max_overflow, pool_recycle)
207
+ - Automatic connection recovery with retry logic (using tenacity)
198
208
  - Session scoping for thread safety
209
+ - Automatic backend_id injection before flush operations
210
+ - Pre-ping health checks to validate connections
211
+ - Support for auto-importing model modules
199
212
 
200
213
  ### `dynamic_table.py` - Dynamic Table Creation
201
214
 
@@ -234,7 +247,7 @@ exists = db.inst_is_in_db(
234
247
 
235
248
  ### `postgres_info.py` - Connection Information
236
249
 
237
- Data class for PostgreSQL connection parameters:
250
+ Class for PostgreSQL connection parameters:
238
251
 
239
252
  ```python
240
253
  from ry_pg_utils.postgres_info import PostgresInfo
@@ -247,8 +260,12 @@ db_info = PostgresInfo(
247
260
  password="secret"
248
261
  )
249
262
 
250
- # Get connection URI
251
- uri = db_info.get_uri() # postgresql://postgres:secret@localhost:5432/mydb
263
+ # Check if valid
264
+ if not db_info.is_null():
265
+ print(db_info) # Prints info with password masked
266
+
267
+ # Create null instance
268
+ null_info = PostgresInfo.null()
252
269
  ```
253
270
 
254
271
  ### `parse_args.py` - Argument Parsing
@@ -268,48 +285,129 @@ args = parser.parse_args()
268
285
 
269
286
  ### `updater.py` - Database Configuration Updater
270
287
 
271
- Dynamically update database connections based on configuration messages:
288
+ Dynamically update database connections based on configuration messages via Redis:
272
289
 
273
290
  ```python
274
- from ry_pg_utils.updater import PostgresDbUpdater
291
+ from ry_pg_utils.updater import DbUpdater
292
+ from ry_redis_bus.helpers import RedisInfo
293
+ from ryutils.verbose import Verbose
275
294
 
276
- updater = PostgresDbUpdater(
295
+ updater = DbUpdater(
277
296
  redis_info=redis_info,
278
- verbose=verbose,
279
- backend_id="my_backend"
297
+ args=args, # argparse.Namespace with postgres config
298
+ backend_id="my_backend",
299
+ verbose=Verbose(True)
300
+ )
301
+
302
+ # Initialize and start listening for configuration updates
303
+ updater.init()
304
+
305
+ # Run the update loop
306
+ updater.run() # Blocks, continuously checking for updates
307
+ ```
308
+
309
+ ### `notify_trigger.py` - PostgreSQL LISTEN/NOTIFY Support
310
+
311
+ Create database triggers that send notifications on table changes:
312
+
313
+ ```python
314
+ from ry_pg_utils.notify_trigger import (
315
+ create_notify_trigger,
316
+ drop_notify_trigger,
317
+ subscribe_to_notifications,
318
+ NotificationListener,
319
+ )
320
+ from ry_pg_utils.connect import get_engine
321
+
322
+ engine = get_engine("myapp_db")
323
+
324
+ # Create a trigger that notifies on INSERT/UPDATE/DELETE
325
+ create_notify_trigger(
326
+ engine=engine,
327
+ table_name="users",
328
+ channel_name="user_changes",
329
+ events=["INSERT", "UPDATE", "DELETE"],
330
+ columns=["id", "username", "email"] # Optional: only include specific columns
331
+ )
332
+
333
+ # Subscribe to notifications
334
+ def handle_notification(notification):
335
+ print(f"Table: {notification['table']}")
336
+ print(f"Action: {notification['action']}")
337
+ print(f"Data: {notification['data']}")
338
+
339
+ with subscribe_to_notifications(
340
+ engine=engine,
341
+ channel_name="user_changes",
342
+ callback=handle_notification,
343
+ timeout=60.0
344
+ ) as notifications:
345
+ # Notifications are handled in background thread
346
+ time.sleep(10)
347
+
348
+ # Or use the NotificationListener class for long-running listeners
349
+ listener = NotificationListener(db_name="myapp_db")
350
+ listener.create_listener(
351
+ table_name="users",
352
+ channel_name="user_changes",
353
+ events=["INSERT", "UPDATE"]
354
+ )
355
+ listener.add_callback("user_changes", handle_notification)
356
+ listener.start()
357
+
358
+ # ... your application code ...
359
+
360
+ listener.stop()
361
+ ```
362
+
363
+ ### `ipc/channels.py` - Redis Communication Channels
364
+
365
+ Pre-defined Redis channels for database configuration updates (requires ry_redis_bus):
366
+
367
+ ```python
368
+ from ry_pg_utils.ipc.channels import (
369
+ DATABASE_CHANNEL, # DatabaseConfigPb messages
370
+ DATABASE_CONFIG_CHANNEL, # DatabaseSettingsPb messages
371
+ DATABASE_NOTIFY_CHANNEL, # DatabaseNotificationPb messages
280
372
  )
281
373
 
282
- # Start listening for configuration updates
283
- updater.run()
374
+ # These channels are used by DbUpdater for dynamic database configuration
284
375
  ```
285
376
 
286
377
  ## Advanced Usage
287
378
 
288
379
  ### Multi-Backend Support
289
380
 
290
- When `add_backend_to_all` is enabled, all tables automatically get a `backend_id` column:
381
+ When `add_backend_to_all` is enabled (default: True), all tables automatically get a `backend_id` column:
291
382
 
292
383
  ```python
293
384
  from ry_pg_utils.connect import set_backend_id, ManagedSession
385
+ from sqlalchemy import text
294
386
 
295
387
  # Set backend ID for current thread
296
388
  set_backend_id("backend_1")
297
389
 
298
390
  # 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")
391
+ # ManagedSession automatically sets the backend_id if provided
392
+ with ManagedSession(db="myapp_db", backend_id="backend_1") as session:
393
+ if session:
394
+ # New/dirty records automatically get backend_id injected before flush
395
+ result = session.execute(text("SELECT * FROM my_table"))
302
396
  ```
303
397
 
304
398
  ### Custom Table Names
305
399
 
306
- When `add_backend_to_tables` is enabled, table names are automatically suffixed:
400
+ When `add_backend_to_tables` is enabled (default: True), table names are automatically suffixed:
307
401
 
308
402
  ```python
309
403
  from ry_pg_utils.connect import get_table_name
404
+ from ry_pg_utils.config import pg_config
310
405
 
311
406
  # Returns "events_my_backend" if add_backend_to_tables=True
312
- table_name = get_table_name("events", backend_id="my_backend")
407
+ table_name = get_table_name("events", backend_id="my_backend", verbose=True)
408
+
409
+ # Configure globally
410
+ pg_config.add_backend_to_tables = False # Disable backend suffix
313
411
  ```
314
412
 
315
413
  ### ORM Base Class
@@ -327,27 +425,53 @@ class User(Base):
327
425
  name = Column(String(100))
328
426
  email = Column(String(200))
329
427
 
330
- # If add_backend_to_all=True, backend_id column is automatically added
428
+ # If add_backend_to_all=True (default), backend_id column is automatically added
429
+ # The backend_id is a String(256) column, nullable=False
430
+ ```
431
+
432
+ ### Auto-Importing Models
433
+
434
+ Use the `models_module` parameter to automatically import all models before table creation:
435
+
436
+ ```python
437
+ from ry_pg_utils.connect import init_database
438
+
439
+ # This will walk through 'myapp.models' and import all submodules
440
+ # ensuring all model classes are registered with Base.metadata
441
+ init_database(
442
+ db_name="myapp_db",
443
+ db_host="localhost",
444
+ db_port=5432,
445
+ db_user="postgres",
446
+ db_password="secret",
447
+ models_module="myapp.models" # Dot-separated module path
448
+ )
331
449
  ```
332
450
 
333
451
  ## Error Handling
334
452
 
335
- The library includes robust error handling:
453
+ The library includes robust error handling with automatic retries:
336
454
 
337
455
  ```python
338
456
  from ry_pg_utils.connect import ManagedSession
457
+ from sqlalchemy import text
339
458
 
340
- with ManagedSession(db="myapp") as session:
459
+ with ManagedSession(db="myapp_db") as session:
341
460
  if session is None:
342
- # Connection failed, handle gracefully
461
+ # Connection failed after retries, handle gracefully
343
462
  print("Failed to establish database connection")
344
463
  return
345
464
 
346
465
  try:
347
- session.execute("SELECT * FROM my_table")
466
+ session.execute(text("SELECT * FROM my_table"))
348
467
  except Exception as e:
349
- # Session will automatically rollback
468
+ # Session will automatically rollback on exception
350
469
  print(f"Query failed: {e}")
470
+
471
+ # Retries are built-in:
472
+ # - Session operations retry 3 times on OperationalError
473
+ # - Exponential backoff: min 4s, max 10s
474
+ # - Connection health checks via pool_pre_ping
351
475
  ```
352
476
 
353
477
  ## Type Safety
@@ -378,12 +502,17 @@ pip install -r packages/requirements-dev.txt
378
502
 
379
503
  ### Running Tests
380
504
 
505
+ Tests require a running PostgreSQL instance. Configure test database connection via environment variables or `.env` file.
506
+
381
507
  ```bash
382
508
  # Activate virtual environment
383
509
  source venv-dev/bin/activate
384
510
 
385
- # Run tests
386
- python -m pytest test/
511
+ # Run all tests
512
+ make test
513
+
514
+ # Run specific test module
515
+ make test TESTMODULE=connect_test
387
516
  ```
388
517
 
389
518
  ### Code Quality
@@ -392,16 +521,12 @@ The project uses several tools for code quality:
392
521
 
393
522
  ```bash
394
523
  # Format code
395
- black ry_pg_utils/
524
+ make format
396
525
 
397
- # Type checking
398
- mypy ry_pg_utils/
526
+ # Run linting
527
+ make lint_full
399
528
 
400
- # Linting
401
- pylint ry_pg_utils/
402
-
403
- # Import sorting
404
- isort ry_pg_utils/
529
+ # Type checking is included in lint_full (uses mypy)
405
530
  ```
406
531
 
407
532
  ## Examples
@@ -412,8 +537,8 @@ isort ry_pg_utils/
412
537
  import argparse
413
538
  from ry_pg_utils.parse_args import add_postrgres_db_args
414
539
  from ry_pg_utils.connect import init_database, ManagedSession
415
- from ry_pg_utils.postgres_info import PostgresInfo
416
540
  from ry_pg_utils.dynamic_table import DynamicTableDb
541
+ from sqlalchemy import text
417
542
 
418
543
  def parse_args():
419
544
  parser = argparse.ArgumentParser(description="My Database App")
@@ -424,26 +549,78 @@ def main():
424
549
  args = parse_args()
425
550
 
426
551
  # Initialize database
427
- db_info = PostgresInfo(
552
+ init_database(
428
553
  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
554
+ db_host=args.postgres_host,
555
+ db_port=args.postgres_port,
556
+ db_user=args.postgres_user,
557
+ db_password=args.postgres_password
433
558
  )
434
559
 
435
- init_database(db_info, db_name="myapp")
436
-
437
560
  # Use the database
438
- with ManagedSession(db="myapp") as session:
561
+ with ManagedSession(db=args.postgres_db) as session:
439
562
  if session:
440
- result = session.execute("SELECT version()")
563
+ result = session.execute(text("SELECT version()"))
441
564
  print(f"PostgreSQL version: {result.fetchone()[0]}")
442
565
 
443
566
  if __name__ == "__main__":
444
567
  main()
445
568
  ```
446
569
 
570
+ ### Real-time Notification Example
571
+
572
+ ```python
573
+ from ry_pg_utils.connect import init_database, get_engine, Base
574
+ from ry_pg_utils.notify_trigger import create_notify_trigger, NotificationListener
575
+ from sqlalchemy import Column, Integer, String
576
+ import time
577
+
578
+ # Define a model
579
+ class Product(Base):
580
+ __tablename__ = 'products'
581
+
582
+ id = Column(Integer, primary_key=True)
583
+ name = Column(String(100))
584
+ price = Column(Integer)
585
+
586
+ # Initialize database
587
+ init_database(
588
+ db_name="inventory_db",
589
+ db_host="localhost",
590
+ db_port=5432,
591
+ db_user="postgres",
592
+ db_password="secret"
593
+ )
594
+
595
+ # Create notification trigger
596
+ engine = get_engine("inventory_db")
597
+ create_notify_trigger(
598
+ engine=engine,
599
+ table_name="products",
600
+ channel_name="product_updates",
601
+ events=["INSERT", "UPDATE", "DELETE"],
602
+ columns=["id", "name", "price"]
603
+ )
604
+
605
+ # Set up listener
606
+ listener = NotificationListener(db_name="inventory_db")
607
+
608
+ def on_product_change(notification):
609
+ action = notification['action']
610
+ data = notification['data']
611
+ print(f"Product {action}: {data}")
612
+
613
+ listener.add_callback("product_updates", on_product_change)
614
+ listener.start()
615
+
616
+ # Your application runs...
617
+ try:
618
+ while True:
619
+ time.sleep(1)
620
+ except KeyboardInterrupt:
621
+ listener.stop()
622
+ ```
623
+
447
624
  ## License
448
625
 
449
626
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -464,10 +641,19 @@ Ross Yeager - ryeager12@email.com
464
641
 
465
642
  ## Changelog
466
643
 
644
+ ### Version 1.0.2 (Current)
645
+ - PostgreSQL LISTEN/NOTIFY support with triggers and notifications
646
+ - NotificationListener class for background notification handling
647
+ - Automatic connection health checks with pool_pre_ping
648
+ - Auto-importing models from specified module paths
649
+ - Enhanced retry logic with tenacity
650
+ - Improved error handling and connection recovery
651
+
467
652
  ### Version 1.0.0
468
653
  - Initial release
469
- - Database connection management
470
- - Dynamic table creation
471
- - Multi-backend support
472
- - Configuration system
654
+ - Database connection management with pooling
655
+ - Dynamic table creation from Protocol Buffers
656
+ - Multi-backend support with automatic ID tagging
657
+ - Configuration system with environment variables
473
658
  - Protocol Buffer integration
659
+ - Redis-based database configuration updates
@@ -8,13 +8,13 @@ ry_pg_utils/postgres_info.py,sha256=mj9er830jvrJXUFuFKx1EmZMjuEpMDcd901kgYb_Cck,
8
8
  ry_pg_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  ry_pg_utils/updater.py,sha256=TbUKJHQarzRrDjcbjbzIhy1sO61o-YhEG594gWnI0LA,5831
10
10
  ry_pg_utils/ipc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- ry_pg_utils/ipc/channels.py,sha256=Qn1SUcxkl_vnM-ZejZalVRVu4sGPuNIWIbxmo8Q0DVI,612
11
+ ry_pg_utils/ipc/channels.py,sha256=sjYuRjmqWMN26v8m6Qm72lecwOJ1dZAF-qfcDR-qjsY,525
12
12
  ry_pg_utils/pb_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  ry_pg_utils/pb_types/database_pb2.py,sha256=yf__HycGrVoU8pzhYdnOhXhfAkgjeGl80ls_RsLemy4,2780
14
14
  ry_pg_utils/pb_types/database_pb2.pyi,sha256=svpmlEY_99dA_u3CJqn7CO5D7CH2pAGTOqlhVMZLXC4,5689
15
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,,
16
+ ry_pg_utils-1.0.3.dist-info/licenses/LICENSE,sha256=PYnig94SABlde939TWrqvOqQkkfjuHttf8KpWKlPFlA,1068
17
+ ry_pg_utils-1.0.3.dist-info/METADATA,sha256=v6jbyX6YQRMNZRSMXhOevKvwVDRS2T8P9j0IOvPwrWM,18037
18
+ ry_pg_utils-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ ry_pg_utils-1.0.3.dist-info/top_level.txt,sha256=OruzbmsQHYyPnAw8RichQ5GK1Sxj-MQwWfJ93PEHXIM,12
20
+ ry_pg_utils-1.0.3.dist-info/RECORD,,