supertable 2.0.5__tar.gz → 2.0.7__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.5/supertable.egg-info → supertable-2.0.7}/PKG-INFO +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/pyproject.toml +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/setup.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/__init__.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/admin.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/logger.py +2 -2
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/retention.py +3 -7
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/test_retention.py +7 -4
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/writer_redis.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/settings.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/data_writer.py +5 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/meta_reader.py +5 -2
- {supertable-2.0.5 → supertable-2.0.7}/supertable/mirroring/mirror_formats.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/monitoring_writer.py +30 -19
- {supertable-2.0.5 → supertable-2.0.7}/supertable/plan_extender.py +4 -2
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/tests/test_rbac.py +1 -1
- {supertable-2.0.5 → supertable-2.0.7}/supertable/redis_catalog.py +17 -39
- {supertable-2.0.5 → supertable-2.0.7}/supertable/redis_infra.py +1 -1
- supertable-2.0.7/supertable/redis_keys.py +810 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/staging_area.py +2 -2
- {supertable-2.0.5 → supertable-2.0.7}/supertable/super_table.py +6 -4
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_data_writer.py +4 -2
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_dedup_on_read_write.py +3 -3
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_meta_reader.py +44 -44
- supertable-2.0.7/supertable/tests/test_redis_key_prefix.py +439 -0
- {supertable-2.0.5 → supertable-2.0.7/supertable.egg-info}/PKG-INFO +1 -1
- supertable-2.0.5/supertable/redis_keys.py +0 -404
- supertable-2.0.5/supertable/tests/test_redis_key_prefix.py +0 -236
- {supertable-2.0.5 → supertable-2.0.7}/LICENSE +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/README.md +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/requirements.txt +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/setup.cfg +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/chain.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/consumers.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/crypto.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/events.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/export.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/middleware.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/reader.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/defaults.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/homedir.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/tests/test_defaults.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/data_classes.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/data_reader.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/data_estimator.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/duckdb_lite.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/duckdb_pro.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/engine_common.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/executor.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/spark_thrift.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/tests/test_dedup_read.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/file_lock.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/logging.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/processing.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/query_plan_manager.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/access_control.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/permissions.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/redis_connector.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/simple_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/local_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/storage/tests/test_storage.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/super_pipe.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_align_to_schema_fix.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_data_reader.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_data_writer_comprehensive.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_data_writer_tombstones.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_process_delete_only.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_processing.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_small_file_compaction.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/tests/test_supertable_all.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/utils/__init__.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/utils/helper.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/utils/sql_parser.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable/utils/timer.py +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable.egg-info/SOURCES.txt +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.0.5 → supertable-2.0.7}/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.7",
|
|
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.7"
|
|
29
29
|
|
|
30
30
|
# Re-export the core public surface so users can do ``from supertable import …``
|
|
31
31
|
# instead of remembering submodule paths.
|
|
@@ -408,7 +408,7 @@ _LOGGERS: Dict[str, "AuditLogger | NullAuditLogger"] = {}
|
|
|
408
408
|
_LOGGERS_LOCK = threading.Lock()
|
|
409
409
|
|
|
410
410
|
# Per-org config cache: org → (config, expires_at_seconds).
|
|
411
|
-
# Resolved against env defaults + Redis override (supertable:{org}:audit:config).
|
|
411
|
+
# Resolved against env defaults + Redis override (supertable:{org}:system:audit:config).
|
|
412
412
|
_ORG_CFG_CACHE: Dict[str, "tuple[AuditConfig, float]"] = {}
|
|
413
413
|
_ORG_CFG_TTL_S: float = 30.0 # toggle takes effect within this many seconds
|
|
414
414
|
|
|
@@ -416,7 +416,7 @@ _ORG_CFG_TTL_S: float = 30.0 # toggle takes effect within this many seconds
|
|
|
416
416
|
def _resolve_config_for(organization: str) -> AuditConfig:
|
|
417
417
|
"""Resolve the effective AuditConfig for *organization*.
|
|
418
418
|
|
|
419
|
-
Merges the Redis override at ``supertable:{org}:audit:config`` over the
|
|
419
|
+
Merges the Redis override at ``supertable:{org}:system:audit:config`` over the
|
|
420
420
|
env-var defaults. Cached for _ORG_CFG_TTL_S seconds.
|
|
421
421
|
"""
|
|
422
422
|
now = time.time()
|
|
@@ -25,17 +25,13 @@ import re
|
|
|
25
25
|
from datetime import datetime, timedelta, timezone
|
|
26
26
|
from typing import Any, Dict, List, Optional
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
from supertable import redis_keys as RK
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
# Redis key for legal hold runtime override
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
_LEGAL_HOLD_KEY_TEMPLATE = "supertable:{org}:audit:legal_hold"
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
def _legal_hold_key(org: str) -> str:
|
|
38
|
-
return
|
|
34
|
+
return RK.audit_legal_hold(org)
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
# ---------------------------------------------------------------------------
|
|
@@ -28,10 +28,13 @@ from supertable.audit import retention
|
|
|
28
28
|
|
|
29
29
|
class TestLegalHoldKey:
|
|
30
30
|
def test_template_substitution(self) -> None:
|
|
31
|
-
assert retention._legal_hold_key("acme") == "supertable:acme:audit:legal_hold"
|
|
31
|
+
assert retention._legal_hold_key("acme") == "supertable:acme:system:audit:legal_hold"
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
33
|
+
def test_handles_hyphens(self) -> None:
|
|
34
|
+
# v2 _safe() requires the org-segment regex
|
|
35
|
+
# ^[a-z0-9][a-z0-9_-]{0,63}$ — uppercase letters are rejected,
|
|
36
|
+
# hyphens + lowercase alphanumerics are allowed.
|
|
37
|
+
assert retention._legal_hold_key("acme-eu") == "supertable:acme-eu:system:audit:legal_hold"
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
class TestParsePartitionDate:
|
|
@@ -171,7 +174,7 @@ class TestSetLegalHold:
|
|
|
171
174
|
assert result == {"ok": True, "legal_hold": True, "organization": "acme"}
|
|
172
175
|
|
|
173
176
|
fake_client.set.assert_called_once_with(
|
|
174
|
-
"supertable:acme:audit:legal_hold", "1"
|
|
177
|
+
"supertable:acme:system:audit:legal_hold", "1"
|
|
175
178
|
)
|
|
176
179
|
assert audit_calls and audit_calls[0]["organization"] == "acme"
|
|
177
180
|
|
|
@@ -6,7 +6,7 @@ Provides real-time queryability (XRANGE), consumer groups for external
|
|
|
6
6
|
SIEM tools (Splunk, Sentinel, ELK), and TTL-based eviction.
|
|
7
7
|
|
|
8
8
|
Each organization gets its own stream:
|
|
9
|
-
supertable:{org}:audit:stream
|
|
9
|
+
supertable:{org}:system:audit:stream
|
|
10
10
|
|
|
11
11
|
The internal archival consumer group ("__archival__") is created
|
|
12
12
|
automatically. External SIEM consumer groups are managed via the
|
|
@@ -261,7 +261,7 @@ class Settings:
|
|
|
261
261
|
|
|
262
262
|
# ── Audit ────────────────────────────────────────────────────────
|
|
263
263
|
# Audit is OFF by default. Enable per-organization in the WebUI
|
|
264
|
-
# /ui/audit → Compliance tab (persisted at supertable:{org}:audit:config),
|
|
264
|
+
# /ui/audit → Compliance tab (persisted at supertable:{org}:system:audit:config),
|
|
265
265
|
# or globally via the SUPERTABLE_AUDIT_ENABLED env var.
|
|
266
266
|
SUPERTABLE_AUDIT_ENABLED: bool = False # SUPERTABLE_AUDIT_ENABLED
|
|
267
267
|
SUPERTABLE_AUDIT_RETENTION_DAYS: int = 2555 # SUPERTABLE_AUDIT_RETENTION_DAYS (~7 years)
|
|
@@ -535,8 +535,12 @@ class DataWriter:
|
|
|
535
535
|
# guaranteed to reach Redis before this scope closes.
|
|
536
536
|
try:
|
|
537
537
|
if stats_payload is not None:
|
|
538
|
+
# Monitoring is org-wide as of SDK 2.2.0 — record the
|
|
539
|
+
# touched supertable in the payload's ``supertables``
|
|
540
|
+
# field. A DataWriter only touches one supertable, but
|
|
541
|
+
# the list shape is uniform with cross-sup events.
|
|
542
|
+
stats_payload["supertables"] = [self.super_table.super_name]
|
|
538
543
|
with MonitoringWriter(
|
|
539
|
-
super_name=self.super_table.super_name,
|
|
540
544
|
organization=self.super_table.organization,
|
|
541
545
|
monitor_type="writes",
|
|
542
546
|
) as monitor:
|
|
@@ -154,7 +154,7 @@ class MetaReader:
|
|
|
154
154
|
count=1000,
|
|
155
155
|
):
|
|
156
156
|
key_str = key if isinstance(key, str) else key.decode('utf-8')
|
|
157
|
-
table_name = key_str.rsplit("meta:leaf:", 1)[-1]
|
|
157
|
+
table_name = key_str.rsplit("meta:leaf:doc:", 1)[-1]
|
|
158
158
|
if table_name and table_name not in seen:
|
|
159
159
|
seen.add(table_name)
|
|
160
160
|
tables.append(table_name)
|
|
@@ -507,7 +507,10 @@ def list_supers(organization: str, role_name: str) -> List[str]:
|
|
|
507
507
|
|
|
508
508
|
items = _get_redis_items(pattern)
|
|
509
509
|
for item in items:
|
|
510
|
-
|
|
510
|
+
parsed = RK.parse_lake_key(item)
|
|
511
|
+
if parsed is None:
|
|
512
|
+
continue
|
|
513
|
+
_, super_name = parsed
|
|
511
514
|
try:
|
|
512
515
|
check_meta_access(
|
|
513
516
|
super_name=super_name,
|
|
@@ -40,7 +40,7 @@ class MirrorFormats:
|
|
|
40
40
|
Redis-backed format mirror configuration and dispatch.
|
|
41
41
|
|
|
42
42
|
Storage:
|
|
43
|
-
- Redis key: supertable:{org}:{super}:meta:mirrors
|
|
43
|
+
- Redis key: supertable:{org}:lakes:{super}:meta:mirrors
|
|
44
44
|
value: {"formats": ["DELTA", ...], "ts": <epoch_ms>}
|
|
45
45
|
"""
|
|
46
46
|
|
|
@@ -109,19 +109,21 @@ class NullMonitoringLogger:
|
|
|
109
109
|
@dataclass(frozen=True)
|
|
110
110
|
class _MonitorKey:
|
|
111
111
|
organization: str
|
|
112
|
-
super_name: str
|
|
113
112
|
monitor_type: str
|
|
114
113
|
|
|
115
114
|
@property
|
|
116
115
|
def path_key(self) -> str:
|
|
117
|
-
# Matches
|
|
118
|
-
return f"{self.organization}/{self.
|
|
116
|
+
# Matches the log style: "org/monitor_type"
|
|
117
|
+
return f"{self.organization}/{self.monitor_type}"
|
|
119
118
|
|
|
120
119
|
@property
|
|
121
120
|
def redis_list_key(self) -> str:
|
|
122
|
-
#
|
|
123
|
-
# supertable:{org}:
|
|
124
|
-
|
|
121
|
+
# v2.2: org-wide monitoring lives at position 2.
|
|
122
|
+
# supertable:{org}:monitor:{monitor_type}
|
|
123
|
+
# Cross-supertable queries record exactly one canonical entry
|
|
124
|
+
# here; attribution is preserved in the payload's
|
|
125
|
+
# ``supertables: [str]`` field.
|
|
126
|
+
return RK.monitor(self.organization, self.monitor_type)
|
|
125
127
|
|
|
126
128
|
|
|
127
129
|
class _AsyncMonitoringLogger:
|
|
@@ -455,7 +457,6 @@ def _monitoring_enabled() -> bool:
|
|
|
455
457
|
|
|
456
458
|
def get_monitoring_logger(
|
|
457
459
|
*,
|
|
458
|
-
super_name: str,
|
|
459
460
|
organization: str,
|
|
460
461
|
monitor_type: str = "plans",
|
|
461
462
|
redis_connector: Optional["RedisConnector"] = None,
|
|
@@ -464,11 +465,16 @@ def get_monitoring_logger(
|
|
|
464
465
|
Return a monitoring logger (context manager + log_metric + request_flush + debug fields).
|
|
465
466
|
|
|
466
467
|
This function never raises; on any failure it returns a NullMonitoringLogger.
|
|
468
|
+
|
|
469
|
+
Per-supertable attribution is encoded **in the payload**, not in
|
|
470
|
+
the Redis key — callers include a ``supertables: [str]`` field in
|
|
471
|
+
each payload passed to ``log_metric``. The org-level shape lets
|
|
472
|
+
cross-supertable queries record a single canonical entry.
|
|
467
473
|
"""
|
|
468
474
|
if not _monitoring_enabled():
|
|
469
475
|
return NullMonitoringLogger()
|
|
470
476
|
|
|
471
|
-
key = _MonitorKey(organization=organization,
|
|
477
|
+
key = _MonitorKey(organization=organization, monitor_type=monitor_type)
|
|
472
478
|
cache_key = key.path_key
|
|
473
479
|
|
|
474
480
|
try:
|
|
@@ -490,33 +496,38 @@ def get_monitoring_logger(
|
|
|
490
496
|
|
|
491
497
|
class MonitoringWriter:
|
|
492
498
|
"""
|
|
493
|
-
|
|
499
|
+
Monitoring façade — org-level writer (SDK 2.2.0+).
|
|
494
500
|
|
|
495
|
-
|
|
496
|
-
mon = MonitoringWriter(
|
|
497
|
-
mon.log_metric({...})
|
|
501
|
+
Usage:
|
|
502
|
+
mon = MonitoringWriter(organization=org, monitor_type="plans")
|
|
503
|
+
mon.log_metric({"supertables": ["sales", "customers"], ...})
|
|
498
504
|
mon.request_flush()
|
|
499
505
|
|
|
500
|
-
|
|
501
|
-
with MonitoringWriter(
|
|
502
|
-
mon.log_metric({...})
|
|
506
|
+
Context-manager style:
|
|
507
|
+
with MonitoringWriter(organization=org, monitor_type="writes") as mon:
|
|
508
|
+
mon.log_metric({"supertables": ["sales"], ...})
|
|
509
|
+
|
|
510
|
+
Per-supertable attribution: the Redis key is org-level
|
|
511
|
+
(``supertable:{org}:monitor:{monitor_type}``). Callers include a
|
|
512
|
+
``supertables: [str]`` field in each payload — the full list of
|
|
513
|
+
supertables the event touches. Cross-supertable queries (JOINs,
|
|
514
|
+
multi-target writes) pass the full list. Events not tied to any
|
|
515
|
+
supertable pass ``supertables: []`` (or omit it — the writer
|
|
516
|
+
will default to an empty list).
|
|
503
517
|
|
|
504
|
-
|
|
518
|
+
Also forwards debug attributes used in examples (queue_stats, etc.).
|
|
505
519
|
"""
|
|
506
520
|
|
|
507
521
|
def __init__(
|
|
508
522
|
self,
|
|
509
523
|
*,
|
|
510
|
-
super_name: str,
|
|
511
524
|
organization: str,
|
|
512
525
|
monitor_type: str = "plans",
|
|
513
526
|
redis_connector: Optional["RedisConnector"] = None,
|
|
514
527
|
):
|
|
515
|
-
self.super_name = super_name
|
|
516
528
|
self.organization = organization
|
|
517
529
|
self.monitor_type = monitor_type
|
|
518
530
|
self._logger: MonitoringLogger = get_monitoring_logger(
|
|
519
|
-
super_name=super_name,
|
|
520
531
|
organization=organization,
|
|
521
532
|
monitor_type=monitor_type,
|
|
522
533
|
redis_connector=redis_connector,
|
|
@@ -123,10 +123,12 @@ def extend_execution_plan(
|
|
|
123
123
|
logger.error("Failed to build monitoring stats payload: %s", e)
|
|
124
124
|
return # nothing else to do safely
|
|
125
125
|
|
|
126
|
-
# Log the metric (buffered; background writer flushes)
|
|
126
|
+
# Log the metric (buffered; background writer flushes).
|
|
127
|
+
# Monitoring is org-wide as of SDK 2.2.0 — the touched supertable
|
|
128
|
+
# is recorded in the payload's ``supertables: [str]`` field.
|
|
127
129
|
try:
|
|
130
|
+
stats["supertables"] = [query_plan_manager.super_name]
|
|
128
131
|
with MonitoringWriter(
|
|
129
|
-
super_name=query_plan_manager.super_name,
|
|
130
132
|
organization=query_plan_manager.organization,
|
|
131
133
|
monitor_type="plans",
|
|
132
134
|
) as monitor:
|
|
@@ -1659,7 +1659,7 @@ class TestKeyNamespace(unittest.TestCase):
|
|
|
1659
1659
|
RK.rbac_role_type_index("o", "s", "admin"),
|
|
1660
1660
|
]
|
|
1661
1661
|
for k in keys:
|
|
1662
|
-
self.assertTrue(k.startswith("supertable:o:s:rbac:"), f"Bad key: {k}")
|
|
1662
|
+
self.assertTrue(k.startswith("supertable:o:lakes:s:rbac:"), f"Bad key: {k}")
|
|
1663
1663
|
|
|
1664
1664
|
def test_rbac_keys_do_not_collide(self):
|
|
1665
1665
|
"""All 8 key patterns for same org/sup produce distinct keys."""
|
|
@@ -117,7 +117,7 @@ return v
|
|
|
117
117
|
# ARGV[1] role_id
|
|
118
118
|
# ARGV[2] now_ms (string)
|
|
119
119
|
# ARGV[3] role_name_lower (or "")
|
|
120
|
-
# ARGV[4] user_doc_key_prefix (e.g. "supertable:{org}:{sup}:rbac:users:doc:")
|
|
120
|
+
# ARGV[4] user_doc_key_prefix (e.g. "supertable:{org}:lakes:{sup}:rbac:users:doc:")
|
|
121
121
|
# KEYS layout:
|
|
122
122
|
# KEYS[1] role_doc_key
|
|
123
123
|
# KEYS[2] role_index_key
|
|
@@ -264,7 +264,7 @@ return 1
|
|
|
264
264
|
timeout_s: int = 30,
|
|
265
265
|
) -> Optional[str]:
|
|
266
266
|
"""Acquire lock for staging/pipe operations:
|
|
267
|
-
supertable:{org}:{sup}:lock:stage:{stage_name}
|
|
267
|
+
supertable:{org}:lakes:{sup}:lock:stage:doc:{stage_name}
|
|
268
268
|
"""
|
|
269
269
|
return self._locker.acquire(RK.lock_stage(org, sup, stage_name), ttl_s=ttl_s, timeout_s=timeout_s)
|
|
270
270
|
|
|
@@ -343,10 +343,10 @@ return 1
|
|
|
343
343
|
cursor, keys = self.r.scan(cursor=cursor, match=pattern, count=500)
|
|
344
344
|
for k in keys:
|
|
345
345
|
ks = k if isinstance(k, str) else k.decode("utf-8")
|
|
346
|
-
|
|
347
|
-
if
|
|
346
|
+
parsed = RK.parse_lake_key(ks)
|
|
347
|
+
if parsed is None:
|
|
348
348
|
continue
|
|
349
|
-
sup_name =
|
|
349
|
+
_, sup_name = parsed
|
|
350
350
|
if sup_name == source_sup:
|
|
351
351
|
continue
|
|
352
352
|
try:
|
|
@@ -685,8 +685,8 @@ return 1
|
|
|
685
685
|
role_name = self.r.hget(key, "role_name") or ""
|
|
686
686
|
if isinstance(role_name, bytes):
|
|
687
687
|
role_name = role_name.decode("utf-8")
|
|
688
|
-
#
|
|
689
|
-
user_doc_key_prefix = RK.
|
|
688
|
+
# The Lua script appends each user_id to this prefix.
|
|
689
|
+
user_doc_key_prefix = RK.rbac_user_doc_prefix(org, sup)
|
|
690
690
|
result = self._rbac_delete_role(
|
|
691
691
|
keys=[
|
|
692
692
|
key,
|
|
@@ -954,7 +954,7 @@ return 1
|
|
|
954
954
|
# ------------- Listings via SCAN -------------
|
|
955
955
|
|
|
956
956
|
def scan_leaf_keys(self, org: str, sup: str, count: int = 1000) -> Iterator[str]:
|
|
957
|
-
"""Yields full Redis keys: supertable:{org}:{sup}:meta:leaf:* (replica-aware)."""
|
|
957
|
+
"""Yields full Redis keys: supertable:{org}:lakes:{sup}:meta:leaf:doc:* (replica-aware)."""
|
|
958
958
|
info = self._resolve_replica_info(org, sup)
|
|
959
959
|
effective_sup = info[0] if info else sup
|
|
960
960
|
allowed = info[1] if info else None
|
|
@@ -967,7 +967,7 @@ return 1
|
|
|
967
967
|
for k in keys:
|
|
968
968
|
ks = k if isinstance(k, str) else k.decode('utf-8')
|
|
969
969
|
if allowed:
|
|
970
|
-
simple = ks.rsplit("meta:leaf:", 1)[-1]
|
|
970
|
+
simple = ks.rsplit("meta:leaf:doc:", 1)[-1]
|
|
971
971
|
if simple not in allowed:
|
|
972
972
|
continue
|
|
973
973
|
yield ks
|
|
@@ -1003,7 +1003,7 @@ return 1
|
|
|
1003
1003
|
continue
|
|
1004
1004
|
try:
|
|
1005
1005
|
obj = json.loads(raw)
|
|
1006
|
-
simple = k.rsplit("meta:leaf:", 1)[-1]
|
|
1006
|
+
simple = k.rsplit("meta:leaf:doc:", 1)[-1]
|
|
1007
1007
|
yield {
|
|
1008
1008
|
"simple": simple,
|
|
1009
1009
|
"version": int(obj.get("version", -1)),
|
|
@@ -1216,44 +1216,22 @@ return 1
|
|
|
1216
1216
|
return None
|
|
1217
1217
|
|
|
1218
1218
|
def list_stagings(self, org: str, sup: str, *, count: int = 1000) -> List[str]:
|
|
1219
|
-
"""List staging names
|
|
1219
|
+
"""List staging names from the staging index set."""
|
|
1220
1220
|
if not (org and sup):
|
|
1221
1221
|
return []
|
|
1222
1222
|
try:
|
|
1223
|
-
names =
|
|
1224
|
-
if names:
|
|
1225
|
-
return sorted({(n if isinstance(n, str) else n.decode('utf-8')) for n in names if n})
|
|
1223
|
+
names = self.r.smembers(RK.staging_index(org, sup)) or set()
|
|
1226
1224
|
except redis.RedisError as e:
|
|
1227
1225
|
logger.error(f"[redis-catalog] list_stagings smembers error: {e}")
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
pattern = RK.staging_pattern(org, sup)
|
|
1231
|
-
seen = set()
|
|
1232
|
-
cursor = 0
|
|
1233
|
-
try:
|
|
1234
|
-
while True:
|
|
1235
|
-
cursor, keys = self.r.scan(cursor=cursor, match=pattern, count=max(1, int(count)))
|
|
1236
|
-
for k in keys or []:
|
|
1237
|
-
kk = k if isinstance(k, str) else k.decode("utf-8")
|
|
1238
|
-
if ":pipe:" in kk:
|
|
1239
|
-
continue
|
|
1240
|
-
if kk.endswith(":meta"):
|
|
1241
|
-
continue
|
|
1242
|
-
name = kk.rsplit("meta:staging:", 1)[-1]
|
|
1243
|
-
if name:
|
|
1244
|
-
seen.add(name)
|
|
1245
|
-
if cursor == 0:
|
|
1246
|
-
break
|
|
1247
|
-
except redis.RedisError as e:
|
|
1248
|
-
logger.error(f"[redis-catalog] list_stagings scan error: {e}")
|
|
1249
|
-
return sorted(seen)
|
|
1226
|
+
return []
|
|
1227
|
+
return sorted({(n if isinstance(n, str) else n.decode('utf-8')) for n in names if n})
|
|
1250
1228
|
|
|
1251
1229
|
def delete_staging_meta(self, org: str, sup: str, staging_name: str, *, count: int = 1000) -> int:
|
|
1252
1230
|
"""Delete staging meta and *all* related keys under the staging prefix.
|
|
1253
1231
|
|
|
1254
1232
|
This removes the staging from the staging index set, deletes the staging meta key,
|
|
1255
1233
|
and deletes any keys matching:
|
|
1256
|
-
supertable:{org}:{sup}:meta:staging:{staging_name}:*
|
|
1234
|
+
supertable:{org}:lakes:{sup}:meta:staging:doc:{staging_name}:*
|
|
1257
1235
|
Returns number of keys deleted (best-effort; does not include SREM).
|
|
1258
1236
|
"""
|
|
1259
1237
|
if not (org and sup and staging_name):
|
|
@@ -1393,7 +1371,7 @@ return 1
|
|
|
1393
1371
|
return 0
|
|
1394
1372
|
|
|
1395
1373
|
# ========================================================================= #
|
|
1396
|
-
# Spark Thrift cluster management (org-scoped: supertable:{org}:spark:thrifts)
|
|
1374
|
+
# Spark Thrift cluster management (org-scoped: supertable:{org}:system:spark:thrifts)
|
|
1397
1375
|
# ========================================================================= #
|
|
1398
1376
|
|
|
1399
1377
|
def register_spark_cluster(self, org: str, cluster_id: str, config: Dict[str, Any]) -> None:
|
|
@@ -1489,7 +1467,7 @@ return 1
|
|
|
1489
1467
|
return candidates[0]
|
|
1490
1468
|
|
|
1491
1469
|
# ========================================================================= #
|
|
1492
|
-
# Spark Plug management (org-scoped: supertable:{org}:spark:plugs)
|
|
1470
|
+
# Spark Plug management (org-scoped: supertable:{org}:system:spark:plugs)
|
|
1493
1471
|
# ========================================================================= #
|
|
1494
1472
|
|
|
1495
1473
|
def register_spark_plug(self, org: str, plug_id: str, config: Dict[str, Any]) -> None:
|
|
@@ -177,7 +177,7 @@ class _FallbackCatalog:
|
|
|
177
177
|
continue
|
|
178
178
|
try:
|
|
179
179
|
obj = json.loads(raw if isinstance(raw, str) else raw.decode("utf-8"))
|
|
180
|
-
simple = k.rsplit("meta:leaf:", 1)[-1]
|
|
180
|
+
simple = k.rsplit("meta:leaf:doc:", 1)[-1]
|
|
181
181
|
yield {
|
|
182
182
|
"simple": simple,
|
|
183
183
|
"version": int(obj.get("version", -1)),
|