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.
- {supertable-2.0.0 → supertable-2.0.2}/PKG-INFO +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/pyproject.toml +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/setup.py +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/supertable/__init__.py +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/consumers.py +3 -3
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/logger.py +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/reader.py +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/retention.py +2 -2
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_retention.py +6 -6
- {supertable-2.0.0 → supertable-2.0.2}/supertable/monitoring_writer.py +8 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/redis_connector.py +71 -2
- supertable-2.0.2/supertable/redis_infra.py +467 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/PKG-INFO +1 -1
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/SOURCES.txt +1 -1
- supertable-2.0.0/supertable/server_common.py +0 -957
- {supertable-2.0.0 → supertable-2.0.2}/LICENSE +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/README.md +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/requirements.txt +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/setup.cfg +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/chain.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/crypto.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/events.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/export.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/middleware.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/audit/writer_redis.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/defaults.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/homedir.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/settings.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_defaults.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/data_classes.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/data_reader.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/data_writer.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/data_estimator.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/duckdb_lite.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/duckdb_pro.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/engine_common.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/executor.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/spark_thrift.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/test_dedup_read.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/file_lock.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/logging.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/meta_reader.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_formats.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/plan_extender.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/processing.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/query_plan_manager.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/access_control.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/permissions.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_rbac.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/redis_catalog.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/service_registry.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/simple_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/staging_area.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/local_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/storage/tests/test_storage.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/super_pipe.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/super_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_reader.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer_comprehensive.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_data_writer_tombstones.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_dedup_on_read_write.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_meta_reader.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_process_delete_only.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_processing.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_small_file_compaction.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/tests/test_supertable_all.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/__init__.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/helper.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/sql_parser.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable/utils/timer.py +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.0.0 → supertable-2.0.2}/supertable.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"""
|
|
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)
|