supertable 2.0.0__tar.gz → 2.0.2__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 (164) hide show
  1. {supertable-2.0.0 → supertable-2.0.2}/PKG-INFO +1 -1
  2. {supertable-2.0.0 → supertable-2.0.2}/pyproject.toml +1 -1
  3. {supertable-2.0.0 → supertable-2.0.2}/setup.py +1 -1
  4. {supertable-2.0.0 → supertable-2.0.2}/supertable/__init__.py +1 -1
  5. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/consumers.py +3 -3
  6. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/logger.py +1 -1
  7. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/reader.py +1 -1
  8. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/retention.py +2 -2
  9. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_retention.py +6 -6
  10. {supertable-2.0.0 → supertable-2.0.2}/supertable/monitoring_writer.py +8 -0
  11. {supertable-2.0.0 → supertable-2.0.2}/supertable/redis_connector.py +71 -2
  12. supertable-2.0.2/supertable/redis_infra.py +467 -0
  13. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/PKG-INFO +1 -1
  14. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/SOURCES.txt +1 -1
  15. supertable-2.0.0/supertable/server_common.py +0 -957
  16. {supertable-2.0.0 → supertable-2.0.2}/LICENSE +0 -0
  17. {supertable-2.0.0 → supertable-2.0.2}/README.md +0 -0
  18. {supertable-2.0.0 → supertable-2.0.2}/requirements.txt +0 -0
  19. {supertable-2.0.0 → supertable-2.0.2}/setup.cfg +0 -0
  20. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/__init__.py +0 -0
  21. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/chain.py +0 -0
  22. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/crypto.py +0 -0
  23. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/events.py +0 -0
  24. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/export.py +0 -0
  25. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/middleware.py +0 -0
  26. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/__init__.py +0 -0
  27. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_chain.py +0 -0
  28. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_crypto.py +0 -0
  29. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_emit.py +0 -0
  30. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_events.py +0 -0
  31. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/writer_parquet.py +0 -0
  32. {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/writer_redis.py +0 -0
  33. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/__init__.py +0 -0
  34. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/defaults.py +0 -0
  35. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/homedir.py +0 -0
  36. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/settings.py +0 -0
  37. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/__init__.py +0 -0
  38. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_defaults.py +0 -0
  39. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_homedir.py +0 -0
  40. {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_settings.py +0 -0
  41. {supertable-2.0.0 → supertable-2.0.2}/supertable/data_classes.py +0 -0
  42. {supertable-2.0.0 → supertable-2.0.2}/supertable/data_reader.py +0 -0
  43. {supertable-2.0.0 → supertable-2.0.2}/supertable/data_writer.py +0 -0
  44. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/__init__.py +0 -0
  45. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/__init__.py +0 -0
  46. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/__main__.py +0 -0
  47. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/check_filter_builder.py +0 -0
  48. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/controller.py +0 -0
  49. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
  50. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/defaults.py +0 -0
  51. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/dummy_data.py +0 -0
  52. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/read_parquet_header.py +0 -0
  53. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
  54. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
  55. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
  56. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
  57. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
  58. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
  59. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
  60. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
  61. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
  62. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
  63. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
  64. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
  65. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
  66. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
  67. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
  68. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
  69. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
  70. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
  71. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
  72. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
  73. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
  74. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
  75. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
  76. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
  77. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/__init__.py +0 -0
  78. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/core.py +0 -0
  79. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/defaults.py +0 -0
  80. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/generate.py +0 -0
  81. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/load.py +0 -0
  82. {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/topup.py +0 -0
  83. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/__init__.py +0 -0
  84. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/data_estimator.py +0 -0
  85. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/duckdb_lite.py +0 -0
  86. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/duckdb_pro.py +0 -0
  87. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/engine_common.py +0 -0
  88. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/engine_enum.py +0 -0
  89. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/executor.py +0 -0
  90. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/plan_stats.py +0 -0
  91. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/spark_thrift.py +0 -0
  92. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/__init__.py +0 -0
  93. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/conftest.py +0 -0
  94. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/test_dedup_read.py +0 -0
  95. {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/test_engine.py +0 -0
  96. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/__init__.py +0 -0
  97. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/__init__.py +0 -0
  98. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
  99. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
  100. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
  101. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/file_lock.py +0 -0
  102. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/redis_lock.py +0 -0
  103. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/__init__.py +0 -0
  104. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/test_file_lock.py +0 -0
  105. {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/test_redis_lock.py +0 -0
  106. {supertable-2.0.0 → supertable-2.0.2}/supertable/logging.py +0 -0
  107. {supertable-2.0.0 → supertable-2.0.2}/supertable/meta_reader.py +0 -0
  108. {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/__init__.py +0 -0
  109. {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_delta.py +0 -0
  110. {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_formats.py +0 -0
  111. {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_iceberg.py +0 -0
  112. {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_parquet.py +0 -0
  113. {supertable-2.0.0 → supertable-2.0.2}/supertable/plan_extender.py +0 -0
  114. {supertable-2.0.0 → supertable-2.0.2}/supertable/processing.py +0 -0
  115. {supertable-2.0.0 → supertable-2.0.2}/supertable/query_plan_manager.py +0 -0
  116. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/__init__.py +0 -0
  117. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/access_control.py +0 -0
  118. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/filter_builder.py +0 -0
  119. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/permissions.py +0 -0
  120. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/role_manager.py +0 -0
  121. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/row_column_security.py +0 -0
  122. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_filter_builder.py +0 -0
  123. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_rbac.py +0 -0
  124. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
  125. {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/user_manager.py +0 -0
  126. {supertable-2.0.0 → supertable-2.0.2}/supertable/redis_catalog.py +0 -0
  127. {supertable-2.0.0 → supertable-2.0.2}/supertable/service_registry.py +0 -0
  128. {supertable-2.0.0 → supertable-2.0.2}/supertable/simple_table.py +0 -0
  129. {supertable-2.0.0 → supertable-2.0.2}/supertable/staging_area.py +0 -0
  130. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/__init__.py +0 -0
  131. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/azure_storage.py +0 -0
  132. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/gcp_storage.py +0 -0
  133. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/local_storage.py +0 -0
  134. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/minio_storage.py +0 -0
  135. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/s3_storage.py +0 -0
  136. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/storage_factory.py +0 -0
  137. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/storage_interface.py +0 -0
  138. {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/tests/test_storage.py +0 -0
  139. {supertable-2.0.0 → supertable-2.0.2}/supertable/super_pipe.py +0 -0
  140. {supertable-2.0.0 → supertable-2.0.2}/supertable/super_table.py +0 -0
  141. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/__init__.py +0 -0
  142. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_reader.py +0 -0
  143. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer.py +0 -0
  144. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer_comprehensive.py +0 -0
  145. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer_tombstones.py +0 -0
  146. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_dedup_on_read_write.py +0 -0
  147. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_meta_reader.py +0 -0
  148. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_newer_than.py +0 -0
  149. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_process_delete_only.py +0 -0
  150. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_processing.py +0 -0
  151. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_query_sql.py +0 -0
  152. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_simple_table.py +0 -0
  153. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_small_file_compaction.py +0 -0
  154. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_super_table.py +0 -0
  155. {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_supertable_all.py +0 -0
  156. {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/__init__.py +0 -0
  157. {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/helper.py +0 -0
  158. {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/sql_parser.py +0 -0
  159. {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
  160. {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/timer.py +0 -0
  161. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/dependency_links.txt +0 -0
  162. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/entry_points.txt +0 -0
  163. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/requires.txt +0 -0
  164. {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supertable
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: SuperTable — versioned data lake library for SQL analytics on Parquet + Redis.
5
5
  Author: Levente Kupas
6
6
  Author-email: Levente Kupas <lkupas@kladnasoft.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "supertable"
7
- version = "2.0.0"
7
+ version = "2.0.2"
8
8
  description = "SuperTable — versioned data lake library for SQL analytics on Parquet + Redis."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,7 +19,7 @@ long_description = readme.read_text(encoding="utf-8") if readme.exists() else ""
19
19
 
20
20
  setup(
21
21
  name="supertable",
22
- version="2.0.0",
22
+ version="2.0.2",
23
23
  description="SuperTable — versioned data lake library for SQL analytics on Parquet + Redis.",
24
24
  long_description=long_description,
25
25
  long_description_content_type="text/markdown",
@@ -25,7 +25,7 @@ See the ``supertable.demo`` package for runnable end-to-end demos and the
25
25
  project documentation for the full API surface.
26
26
  """
27
27
 
28
- __version__ = "2.0.0"
28
+ __version__ = "2.0.2"
29
29
 
30
30
  # Re-export the core public surface so users can do ``from supertable import …``
31
31
  # instead of remembering submodule paths.
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
21
21
  def create_consumer(organization: str, group_name: str, start_from: str = "$") -> Dict[str, Any]:
22
22
  """Create an external SIEM consumer group on the audit stream."""
23
23
  try:
24
- from supertable.server_common import redis_client
24
+ from supertable.redis_infra import redis_client
25
25
  from supertable.audit.writer_redis import RedisAuditWriter
26
26
  writer = RedisAuditWriter(redis_client, organization, "", maxlen=0)
27
27
  ok = writer.create_consumer_group(group_name, start_from)
@@ -34,7 +34,7 @@ def create_consumer(organization: str, group_name: str, start_from: str = "$") -
34
34
  def delete_consumer(organization: str, group_name: str) -> Dict[str, Any]:
35
35
  """Remove an external SIEM consumer group."""
36
36
  try:
37
- from supertable.server_common import redis_client
37
+ from supertable.redis_infra import redis_client
38
38
  from supertable.audit.writer_redis import RedisAuditWriter
39
39
  writer = RedisAuditWriter(redis_client, organization, "", maxlen=0)
40
40
  ok = writer.delete_consumer_group(group_name)
@@ -47,7 +47,7 @@ def delete_consumer(organization: str, group_name: str) -> Dict[str, Any]:
47
47
  def list_consumers(organization: str) -> List[Dict[str, Any]]:
48
48
  """List all consumer groups with lag info."""
49
49
  try:
50
- from supertable.server_common import redis_client
50
+ from supertable.redis_infra import redis_client
51
51
  from supertable.audit.writer_redis import RedisAuditWriter
52
52
  writer = RedisAuditWriter(redis_client, organization, "", maxlen=0)
53
53
  return writer.list_consumer_groups()
@@ -132,7 +132,7 @@ class AuditLogger:
132
132
  def _init_redis(self) -> None:
133
133
  """Initialize Redis Stream writer."""
134
134
  try:
135
- from supertable.server_common import redis_client
135
+ from supertable.redis_infra import redis_client
136
136
  from supertable.audit.writer_redis import RedisAuditWriter
137
137
  self._redis_writer = RedisAuditWriter(
138
138
  redis_client=redis_client,
@@ -93,7 +93,7 @@ def _query_redis(
93
93
  ) -> List[Dict[str, Any]]:
94
94
  """Query recent events from the Redis Stream."""
95
95
  try:
96
- from supertable.server_common import redis_client
96
+ from supertable.redis_infra import redis_client
97
97
  from supertable.audit.writer_redis import RedisAuditWriter
98
98
  writer = RedisAuditWriter(redis_client, organization, "", maxlen=0)
99
99
  return writer.query(start_ms=start_ms, end_ms=end_ms, count=limit)
@@ -86,7 +86,7 @@ def is_legal_hold_active(organization: str) -> bool:
86
86
  """
87
87
  # Try Redis first (runtime override)
88
88
  try:
89
- from supertable.server_common import redis_client
89
+ from supertable.redis_infra import redis_client
90
90
  val = redis_client.get(_legal_hold_key(organization))
91
91
  if val is not None:
92
92
  decoded = val if isinstance(val, str) else val.decode("utf-8")
@@ -128,7 +128,7 @@ def set_legal_hold(enabled: bool, organization: str = "") -> Dict[str, Any]:
128
128
 
129
129
  # Persist to Redis
130
130
  try:
131
- from supertable.server_common import redis_client
131
+ from supertable.redis_infra import redis_client
132
132
  redis_client.set(_legal_hold_key(organization), "1" if enabled else "0")
133
133
  except Exception as e:
134
134
  logger.error("[audit-retention] Failed to persist legal hold to Redis: %s", e)
@@ -83,7 +83,7 @@ class TestIsLegalHoldActive:
83
83
  ) -> None:
84
84
  fake_client = _make_redis_client(get_value=redis_value)
85
85
  fake_module = SimpleNamespace(redis_client=fake_client)
86
- monkeypatch.setitem(__import__("sys").modules, "supertable.server_common", fake_module)
86
+ monkeypatch.setitem(__import__("sys").modules, "supertable.redis_infra", fake_module)
87
87
  assert retention.is_legal_hold_active("acme") is True
88
88
 
89
89
  @pytest.mark.parametrize("redis_value", [b"0", "false", b"no"])
@@ -92,7 +92,7 @@ class TestIsLegalHoldActive:
92
92
  ) -> None:
93
93
  fake_client = _make_redis_client(get_value=redis_value)
94
94
  fake_module = SimpleNamespace(redis_client=fake_client)
95
- monkeypatch.setitem(__import__("sys").modules, "supertable.server_common", fake_module)
95
+ monkeypatch.setitem(__import__("sys").modules, "supertable.redis_infra", fake_module)
96
96
  assert retention.is_legal_hold_active("acme") is False
97
97
 
98
98
  def test_falls_back_to_settings_when_redis_returns_none(
@@ -102,7 +102,7 @@ class TestIsLegalHoldActive:
102
102
  fake_server_common = SimpleNamespace(redis_client=fake_client)
103
103
  monkeypatch.setitem(
104
104
  __import__("sys").modules,
105
- "supertable.server_common",
105
+ "supertable.redis_infra",
106
106
  fake_server_common,
107
107
  )
108
108
 
@@ -125,7 +125,7 @@ class TestIsLegalHoldActive:
125
125
  raise RuntimeError("redis unreachable")
126
126
 
127
127
  monkeypatch.setitem(
128
- __import__("sys").modules, "supertable.server_common", BoomModule()
128
+ __import__("sys").modules, "supertable.redis_infra", BoomModule()
129
129
  )
130
130
 
131
131
  # Make settings import fail by removing the attribute
@@ -155,7 +155,7 @@ class TestSetLegalHold:
155
155
  ) -> None:
156
156
  fake_client = _make_redis_client()
157
157
  fake_module = SimpleNamespace(redis_client=fake_client)
158
- monkeypatch.setitem(__import__("sys").modules, "supertable.server_common", fake_module)
158
+ monkeypatch.setitem(__import__("sys").modules, "supertable.redis_infra", fake_module)
159
159
 
160
160
  # Stub out the audit emit chain so the test never touches the real logger.
161
161
  import supertable.audit as audit_pkg
@@ -181,7 +181,7 @@ class TestSetLegalHold:
181
181
  fake_client = MagicMock()
182
182
  fake_client.set.side_effect = RuntimeError("redis down")
183
183
  fake_module = SimpleNamespace(redis_client=fake_client)
184
- monkeypatch.setitem(__import__("sys").modules, "supertable.server_common", fake_module)
184
+ monkeypatch.setitem(__import__("sys").modules, "supertable.redis_infra", fake_module)
185
185
 
186
186
  result = retention.set_legal_hold(False, organization="acme")
187
187
  assert result["ok"] is False
@@ -425,6 +425,14 @@ def _evict_oldest_monitor() -> None:
425
425
 
426
426
  Strategy: evict the first key (oldest insertion in dict-order, Python 3.7+).
427
427
  Signal the evicted logger's worker thread to stop so it doesn't leak.
428
+
429
+ Note: we intentionally do NOT close the evicted logger's Redis client.
430
+ Since supertable.redis_connector.create_redis_client caches one
431
+ redis.Redis (and one ConnectionPool) per effective RedisOptions, that
432
+ client is shared with every other monitor / catalog / writer in this
433
+ process. Disconnecting it here would tear down their connections too.
434
+ Call supertable.redis_connector.close_all_redis_clients() at process
435
+ shutdown if a clean teardown is required.
428
436
  """
429
437
  if not _MONITORS:
430
438
  return
@@ -2,9 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import os
5
+ import threading
5
6
  import time
6
7
  from dataclasses import dataclass, field
7
- from typing import List, Optional
8
+ from typing import Any, Dict, List, Optional, Tuple
8
9
 
9
10
 
10
11
  import redis
@@ -102,10 +103,47 @@ class RedisOptions:
102
103
  object.__setattr__(self, "sentinel_strict", sentinel_strict)
103
104
 
104
105
 
106
+ # Process-level cache: identical RedisOptions → identical client (and pool).
107
+ # Multiple writer/catalog instances share one ConnectionPool instead of each
108
+ # constructing a new redis.Redis (which would leak its own pool until GC).
109
+ _CLIENT_CACHE: Dict[Tuple[Any, ...], redis.Redis] = {}
110
+ _CLIENT_CACHE_LOCK = threading.Lock()
111
+
112
+
113
+ def _options_cache_key(opts: RedisOptions) -> Tuple[Any, ...]:
114
+ return (
115
+ opts.host,
116
+ opts.port,
117
+ opts.db,
118
+ opts.password,
119
+ opts.use_ssl,
120
+ opts.is_sentinel,
121
+ tuple(opts.sentinel_hosts),
122
+ opts.sentinel_master,
123
+ opts.sentinel_password,
124
+ opts.sentinel_strict,
125
+ )
126
+
127
+
105
128
  def create_redis_client(options: Optional[RedisOptions] = None) -> redis.Redis:
106
129
  opts = options or RedisOptions()
107
130
  decode_responses = True
108
131
 
132
+ cache_key = _options_cache_key(opts)
133
+ cached = _CLIENT_CACHE.get(cache_key)
134
+ if cached is not None:
135
+ return cached
136
+
137
+ with _CLIENT_CACHE_LOCK:
138
+ cached = _CLIENT_CACHE.get(cache_key)
139
+ if cached is not None:
140
+ return cached
141
+ client = _build_redis_client(opts, decode_responses)
142
+ _CLIENT_CACHE[cache_key] = client
143
+ return client
144
+
145
+
146
+ def _build_redis_client(opts: RedisOptions, decode_responses: bool) -> redis.Redis:
109
147
  # Decide between standard Redis and Sentinel-based Redis
110
148
  if opts.is_sentinel and opts.sentinel_hosts:
111
149
  logger.debug(
@@ -202,8 +240,39 @@ def create_redis_client(options: Optional[RedisOptions] = None) -> redis.Redis:
202
240
  )
203
241
 
204
242
 
243
+ def close_all_redis_clients() -> None:
244
+ """Close every cached Redis client and disconnect its pool.
245
+
246
+ Intended for clean process shutdown only. After this call any subsequent
247
+ create_redis_client() will rebuild fresh clients on demand.
248
+ """
249
+ with _CLIENT_CACHE_LOCK:
250
+ clients = list(_CLIENT_CACHE.values())
251
+ _CLIENT_CACHE.clear()
252
+
253
+ for client in clients:
254
+ try:
255
+ pool = getattr(client, "connection_pool", None)
256
+ if pool is not None:
257
+ pool.disconnect()
258
+ except Exception as e:
259
+ logger.debug(f"[redis-connector] pool disconnect failed: {e}")
260
+ try:
261
+ close = getattr(client, "close", None)
262
+ if callable(close):
263
+ close()
264
+ except Exception as e:
265
+ logger.debug(f"[redis-connector] client close failed: {e}")
266
+
267
+
205
268
  class RedisConnector:
206
- """Creates and holds a Redis client connection based on RedisOptions."""
269
+ """Returns a process-shared Redis client based on RedisOptions.
270
+
271
+ The underlying redis.Redis (and its ConnectionPool) is cached by the
272
+ effective options inside create_redis_client, so repeated construction
273
+ of RedisConnector / RedisCatalog / DataWriter / monitoring loggers does
274
+ not allocate new pools or sockets.
275
+ """
207
276
 
208
277
  def __init__(self, options: Optional[RedisOptions] = None):
209
278
  self.r = create_redis_client(options)