supertable 2.0.3__tar.gz → 2.0.5__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.3/supertable.egg-info → supertable-2.0.5}/PKG-INFO +1 -1
- {supertable-2.0.3 → supertable-2.0.5}/pyproject.toml +1 -1
- {supertable-2.0.3 → supertable-2.0.5}/setup.py +1 -1
- {supertable-2.0.3 → supertable-2.0.5}/supertable/__init__.py +1 -1
- {supertable-2.0.3 → supertable-2.0.5}/supertable/processing.py +103 -11
- {supertable-2.0.3 → supertable-2.0.5}/supertable/redis_infra.py +24 -7
- supertable-2.0.5/supertable/redis_keys.py +404 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/super_table.py +12 -0
- supertable-2.0.5/supertable/tests/test_align_to_schema_fix.py +475 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_redis_key_prefix.py +79 -7
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_supertable_all.py +2 -2
- {supertable-2.0.3 → supertable-2.0.5/supertable.egg-info}/PKG-INFO +1 -1
- {supertable-2.0.3 → supertable-2.0.5}/supertable.egg-info/SOURCES.txt +1 -1
- supertable-2.0.3/supertable/redis_keys.py +0 -286
- supertable-2.0.3/supertable/service_registry.py +0 -253
- {supertable-2.0.3 → supertable-2.0.5}/LICENSE +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/README.md +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/requirements.txt +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/setup.cfg +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/admin.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/chain.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/consumers.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/crypto.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/events.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/export.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/logger.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/middleware.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/reader.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/retention.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/tests/test_retention.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/audit/writer_redis.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/defaults.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/homedir.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/settings.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/tests/test_defaults.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/data_classes.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/data_reader.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/data_writer.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/data_estimator.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/duckdb_lite.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/duckdb_pro.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/engine_common.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/executor.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/spark_thrift.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/tests/test_dedup_read.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/file_lock.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/logging.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/meta_reader.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/mirroring/mirror_formats.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/monitoring_writer.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/plan_extender.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/query_plan_manager.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/access_control.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/permissions.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/tests/test_rbac.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/redis_catalog.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/redis_connector.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/simple_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/staging_area.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/local_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/storage/tests/test_storage.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/super_pipe.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_data_reader.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_data_writer.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_data_writer_comprehensive.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_data_writer_tombstones.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_dedup_on_read_write.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_meta_reader.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_process_delete_only.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_processing.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_small_file_compaction.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/utils/__init__.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/utils/helper.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/utils/sql_parser.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable/utils/timer.py +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.0.3 → supertable-2.0.5}/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.5",
|
|
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.5"
|
|
29
29
|
|
|
30
30
|
# Re-export the core public surface so users can do ``from supertable import …``
|
|
31
31
|
# instead of remembering submodule paths.
|
|
@@ -75,37 +75,129 @@ def _resolve_unified_dtype(dtypes: Set[polars.DataType]) -> polars.DataType:
|
|
|
75
75
|
return polars.Utf8
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
|
|
78
|
+
def _union_schema_many(frames: List[polars.DataFrame]) -> Dict[str, polars.DataType]:
|
|
79
|
+
"""Build a unified column-name → dtype mapping across N dataframes.
|
|
80
|
+
|
|
81
|
+
The output dict preserves first-appearance order: a column that first
|
|
82
|
+
appears in frame *i* takes position determined by frame *i*'s own order
|
|
83
|
+
relative to columns that appeared earlier. Dtypes are widened via
|
|
84
|
+
``_resolve_unified_dtype`` over the set of dtypes the column carries
|
|
85
|
+
across all frames that contain it.
|
|
86
|
+
"""
|
|
87
|
+
seen: Set[str] = set()
|
|
88
|
+
cols: List[str] = []
|
|
89
|
+
for f in frames:
|
|
90
|
+
for c in f.columns:
|
|
91
|
+
if c not in seen:
|
|
92
|
+
seen.add(c)
|
|
93
|
+
cols.append(c)
|
|
80
94
|
target: Dict[str, polars.DataType] = {}
|
|
81
95
|
for c in cols:
|
|
82
96
|
types: Set[polars.DataType] = set()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
types.add(b[c].dtype)
|
|
97
|
+
for f in frames:
|
|
98
|
+
if c in f.columns:
|
|
99
|
+
types.add(f[c].dtype)
|
|
87
100
|
target[c] = _resolve_unified_dtype(types)
|
|
88
101
|
return target
|
|
89
102
|
|
|
90
103
|
|
|
104
|
+
def _union_schema(a: polars.DataFrame, b: polars.DataFrame) -> Dict[str, polars.DataType]:
|
|
105
|
+
return _union_schema_many([a, b])
|
|
106
|
+
|
|
107
|
+
|
|
91
108
|
def _align_to_schema(df: polars.DataFrame, target_schema: Dict[str, polars.DataType]) -> polars.DataFrame:
|
|
92
|
-
|
|
109
|
+
"""Project *df* into *target_schema*: same column names, same order, same dtypes.
|
|
110
|
+
|
|
111
|
+
For every column in *target_schema*:
|
|
112
|
+
- present in *df* with the target dtype → keep the existing series
|
|
113
|
+
- present in *df* with a different dtype → cast (strict=False, so unconvertible values become null)
|
|
114
|
+
- absent in *df* → fill with a typed null literal
|
|
115
|
+
|
|
116
|
+
The resulting frame's column order is **exactly** ``list(target_schema.keys())``.
|
|
117
|
+
This is the contract callers like :func:`concat_with_union` rely on:
|
|
118
|
+
``polars.concat(..., how="vertical_relaxed")`` aligns frames *positionally*,
|
|
119
|
+
so it requires identical names at identical positions.
|
|
120
|
+
|
|
121
|
+
Implementation note: ``df.select(exprs)`` is used (not ``with_columns``).
|
|
122
|
+
``with_columns`` preserves the input frame's column order and appends new
|
|
123
|
+
columns at the end, which silently breaks the positional-concat contract
|
|
124
|
+
when *df*'s order disagrees with *target_schema*'s order.
|
|
125
|
+
"""
|
|
126
|
+
if not target_schema:
|
|
127
|
+
return df
|
|
128
|
+
# Zero-row defence: ``df.select([pl.lit(None), ...])`` on an empty frame
|
|
129
|
+
# broadcasts the literal to a single null row, which would silently turn
|
|
130
|
+
# a 0-row input into a 1-row output. Materialise an explicit empty frame
|
|
131
|
+
# with the target schema instead.
|
|
132
|
+
if df.height == 0:
|
|
133
|
+
return polars.DataFrame(schema=target_schema)
|
|
134
|
+
exprs: List[polars.Expr] = []
|
|
93
135
|
for col, dtype in target_schema.items():
|
|
94
136
|
if col in df.columns:
|
|
95
|
-
if df[col]
|
|
137
|
+
if df.schema[col] != dtype:
|
|
96
138
|
exprs.append(polars.col(col).cast(dtype, strict=False))
|
|
139
|
+
else:
|
|
140
|
+
exprs.append(polars.col(col))
|
|
97
141
|
else:
|
|
98
142
|
exprs.append(polars.lit(None, dtype=dtype).alias(col))
|
|
99
|
-
return df.
|
|
143
|
+
return df.select(exprs)
|
|
100
144
|
|
|
101
145
|
|
|
102
146
|
def concat_with_union(a: polars.DataFrame, b: polars.DataFrame) -> polars.DataFrame:
|
|
147
|
+
"""Vertically concatenate two frames with a unified schema.
|
|
148
|
+
|
|
149
|
+
Computes the union of *a*'s and *b*'s schemas, aligns both frames to it
|
|
150
|
+
(filling missing columns with nulls and widening conflicting dtypes), and
|
|
151
|
+
then concatenates positionally. After the union both frames have
|
|
152
|
+
identical columns in identical positions, so the concat cannot fail with
|
|
153
|
+
``schema names differ``.
|
|
154
|
+
"""
|
|
103
155
|
if a.height == 0:
|
|
104
156
|
return b
|
|
105
157
|
if b.height == 0:
|
|
106
158
|
return a
|
|
107
|
-
target =
|
|
108
|
-
return polars.concat(
|
|
159
|
+
target = _union_schema_many([a, b])
|
|
160
|
+
return polars.concat(
|
|
161
|
+
[_align_to_schema(a, target), _align_to_schema(b, target)],
|
|
162
|
+
how="vertical_relaxed",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def concat_many_with_union(frames: List[polars.DataFrame]) -> polars.DataFrame:
|
|
167
|
+
"""Vertically concatenate N frames with a single unified schema.
|
|
168
|
+
|
|
169
|
+
Equivalent to repeated :func:`concat_with_union` but computes the union
|
|
170
|
+
schema once across all inputs (rather than re-deriving it pairwise), and
|
|
171
|
+
issues a single ``polars.concat``. Use this when merging an arbitrary
|
|
172
|
+
set of parquet files with potentially different / dynamic column sets
|
|
173
|
+
(e.g. GA4-style ``param_*`` dynamic columns where each batch contains a
|
|
174
|
+
different subset of keys).
|
|
175
|
+
|
|
176
|
+
Semantics:
|
|
177
|
+
- Empty frames are skipped.
|
|
178
|
+
- If all frames are empty, an empty frame with the union schema is returned.
|
|
179
|
+
- If no frames are given, an empty zero-column frame is returned.
|
|
180
|
+
|
|
181
|
+
Note on memory: this materialises every input frame in memory at once.
|
|
182
|
+
For memory-bounded streaming compaction, callers should still iterate
|
|
183
|
+
with chunked flushes via :func:`concat_with_union` — this helper is for
|
|
184
|
+
callers that already have all frames in memory.
|
|
185
|
+
"""
|
|
186
|
+
if not frames:
|
|
187
|
+
return polars.DataFrame()
|
|
188
|
+
non_empty = [f for f in frames if f.height > 0]
|
|
189
|
+
if not non_empty:
|
|
190
|
+
# All inputs are empty — return an empty frame carrying the union schema
|
|
191
|
+
target = _union_schema_many(frames)
|
|
192
|
+
return polars.DataFrame(schema=target)
|
|
193
|
+
if len(non_empty) == 1:
|
|
194
|
+
# Still project to its own schema explicitly so the output dtype map is
|
|
195
|
+
# the same shape as the multi-frame path (callers can rely on it).
|
|
196
|
+
target = _union_schema_many(non_empty)
|
|
197
|
+
return _align_to_schema(non_empty[0], target)
|
|
198
|
+
target = _union_schema_many(non_empty)
|
|
199
|
+
aligned = [_align_to_schema(f, target) for f in non_empty]
|
|
200
|
+
return polars.concat(aligned, how="vertical_relaxed")
|
|
109
201
|
|
|
110
202
|
|
|
111
203
|
# =========================
|
|
@@ -64,13 +64,25 @@ if settings.SUPERTABLE_LOGIN_MASK not in (1, 2, 3):
|
|
|
64
64
|
f"Invalid SUPERTABLE_LOGIN_MASK (must be 1, 2, or 3): {settings.SUPERTABLE_LOGIN_MASK}"
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
def _require_runtime_env() -> None:
|
|
68
|
+
"""Validate the env vars needed to actually talk to Redis.
|
|
69
|
+
|
|
70
|
+
Called lazily by code paths that open a real Redis connection or
|
|
71
|
+
otherwise need the deployment's organization / superuser token.
|
|
72
|
+
Importing the SDK no longer fails when these are unset — only
|
|
73
|
+
running against Redis does. This keeps the module importable in
|
|
74
|
+
test, build, and inspection contexts where the runtime
|
|
75
|
+
credentials aren't (and shouldn't be) present.
|
|
76
|
+
"""
|
|
77
|
+
missing: List[str] = []
|
|
78
|
+
if not settings.SUPERTABLE_ORGANIZATION:
|
|
79
|
+
missing.append("SUPERTABLE_ORGANIZATION")
|
|
80
|
+
if not (settings.SUPERTABLE_SUPERUSER_TOKEN or "").strip():
|
|
81
|
+
missing.append("SUPERTABLE_SUPERUSER_TOKEN")
|
|
82
|
+
if missing:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
"Missing required environment variables: " + ", ".join(missing)
|
|
85
|
+
)
|
|
74
86
|
|
|
75
87
|
|
|
76
88
|
def _now_ms() -> int:
|
|
@@ -297,6 +309,11 @@ def _build_redis_client() -> redis.Redis:
|
|
|
297
309
|
If SUPERTABLE_REDIS_SENTINEL is enabled and SUPERTABLE_REDIS_SENTINELS is set,
|
|
298
310
|
use the same Sentinel connection behavior as RedisCatalog (ping-probe + optional fallback).
|
|
299
311
|
"""
|
|
312
|
+
# Gate runtime-credential validation here so that simply importing the
|
|
313
|
+
# module (e.g. to inspect ``redis_keys`` helpers or run unit tests
|
|
314
|
+
# against the public API) does not require an organization + token to
|
|
315
|
+
# be set. Anything that actually opens a connection still fails fast.
|
|
316
|
+
_require_runtime_env()
|
|
300
317
|
settings = Settings()
|
|
301
318
|
url = (settings.SUPERTABLE_REDIS_URL or "").strip() or None
|
|
302
319
|
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# route: supertable.redis_keys
|
|
2
|
+
"""
|
|
3
|
+
Single source of truth for every Redis key used by SuperTable + the
|
|
4
|
+
platform layer (dataisland-core).
|
|
5
|
+
|
|
6
|
+
Rule (enforced by tests/test_redis_key_prefix.py):
|
|
7
|
+
Every key constructed here MUST start with one of the two recognised
|
|
8
|
+
top-level prefixes:
|
|
9
|
+
* ``supertable:`` — SuperTable SDK state (catalog, RBAC, locks,
|
|
10
|
+
audit, share federation, table meta, …).
|
|
11
|
+
* ``dataisland:`` — platform / dataisland-core state that is not
|
|
12
|
+
SuperTable's concern (service registry, app
|
|
13
|
+
bootstrap config, etc.).
|
|
14
|
+
Per-app config keys (lighthouse, gatekeeper, studio, …) live under
|
|
15
|
+
their own app-name prefix and are written via the MCP server's
|
|
16
|
+
``store_app_config`` tool — they do **not** go through this module.
|
|
17
|
+
|
|
18
|
+
Hierarchy
|
|
19
|
+
---------
|
|
20
|
+
|
|
21
|
+
dataisland:
|
|
22
|
+
apps:{app_name}:master_mcp ← per-app bootstrap (one global key)
|
|
23
|
+
{org}:
|
|
24
|
+
registry:{service_type}:{host}:{pid} ← service-instance heartbeats (TTL)
|
|
25
|
+
|
|
26
|
+
supertable:
|
|
27
|
+
{org}: ← organization / company scope
|
|
28
|
+
_system_: ← reserved system scope (supertable-side)
|
|
29
|
+
auth:tokens ← organization login tokens
|
|
30
|
+
shares:doc:{share_id}
|
|
31
|
+
shares:index
|
|
32
|
+
audit:stream
|
|
33
|
+
audit:chain_head:{instance_id}
|
|
34
|
+
audit:config ← runtime toggle (per org)
|
|
35
|
+
spark:thrifts
|
|
36
|
+
spark:plugs
|
|
37
|
+
{sup}: ← supertable scope
|
|
38
|
+
meta:root
|
|
39
|
+
meta:leaf:{simple}
|
|
40
|
+
meta:mirrors
|
|
41
|
+
meta:table_config:{simple}
|
|
42
|
+
meta:staging:{staging_name}
|
|
43
|
+
meta:staging:meta
|
|
44
|
+
meta:staging:{staging_name}:pipe:{pipe_name}
|
|
45
|
+
meta:staging:{staging_name}:pipe:meta
|
|
46
|
+
config:engine
|
|
47
|
+
lock:leaf:{simple}
|
|
48
|
+
lock:stage:{stage_name}
|
|
49
|
+
rbac:users:meta
|
|
50
|
+
rbac:users:index
|
|
51
|
+
rbac:users:doc:{user_id}
|
|
52
|
+
rbac:users:name_to_id
|
|
53
|
+
rbac:roles:meta
|
|
54
|
+
rbac:roles:index
|
|
55
|
+
rbac:roles:doc:{role_id}
|
|
56
|
+
rbac:roles:type:{role_type}
|
|
57
|
+
rbac:roles:name_to_id
|
|
58
|
+
schema:{simple}
|
|
59
|
+
table_names
|
|
60
|
+
linked_shares:doc:{link_id}
|
|
61
|
+
linked_shares:index
|
|
62
|
+
monitor:{monitor_type}
|
|
63
|
+
|
|
64
|
+
Reserved supertable names
|
|
65
|
+
-------------------------
|
|
66
|
+
|
|
67
|
+
``_system_`` is reserved. It MUST NOT be used as a supertable name and
|
|
68
|
+
``SuperTable(..., super_name="_system_")`` raises ``ValueError``. The
|
|
69
|
+
name is reserved because we keep everything system-related — service
|
|
70
|
+
registry heartbeats, organization-level auth tokens, future
|
|
71
|
+
system-only scopes — under that prefix to avoid collisions with
|
|
72
|
+
user-created supertables.
|
|
73
|
+
"""
|
|
74
|
+
from __future__ import annotations
|
|
75
|
+
|
|
76
|
+
from typing import FrozenSet
|
|
77
|
+
|
|
78
|
+
# The canonical SuperTable SDK prefix. Everything the SDK itself writes
|
|
79
|
+
# lives under this.
|
|
80
|
+
SUPERTABLE_PREFIX: str = "supertable"
|
|
81
|
+
|
|
82
|
+
# The platform-layer prefix. dataisland-core writes its own
|
|
83
|
+
# infrastructure (service-registry heartbeats, app-bootstrap configs)
|
|
84
|
+
# here. Per-app config keys (lighthouse:*, gatekeeper:*, …) live in
|
|
85
|
+
# yet another set of namespaces and are not built through this module.
|
|
86
|
+
DATAISLAND_PREFIX: str = "dataisland"
|
|
87
|
+
|
|
88
|
+
# Recognised top-level prefixes for assert_prefixed(). Adding a new
|
|
89
|
+
# layer to the platform → extend this set.
|
|
90
|
+
_RECOGNISED_PREFIXES: FrozenSet[str] = frozenset({SUPERTABLE_PREFIX, DATAISLAND_PREFIX})
|
|
91
|
+
|
|
92
|
+
# The org-level system scope. Reserved as a supertable name (see
|
|
93
|
+
# ``is_reserved_super_name`` below). Everything system-related under an
|
|
94
|
+
# organization lives here so user-supplied supertable names can never
|
|
95
|
+
# collide with infrastructure keys.
|
|
96
|
+
SYSTEM_SCOPE: str = "_system_"
|
|
97
|
+
|
|
98
|
+
# All names that may NOT be used for user-created supertables.
|
|
99
|
+
RESERVED_SUPER_NAMES: FrozenSet[str] = frozenset({SYSTEM_SCOPE})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# --------------------------------------------------------------------------- #
|
|
103
|
+
# Guards
|
|
104
|
+
# --------------------------------------------------------------------------- #
|
|
105
|
+
|
|
106
|
+
def assert_prefixed(key: str) -> str:
|
|
107
|
+
"""Raise ValueError if *key* does not start with a recognised prefix.
|
|
108
|
+
|
|
109
|
+
Recognised prefixes: ``supertable:`` (SDK state) and
|
|
110
|
+
``dataisland:`` (platform state). Returns the key unchanged so
|
|
111
|
+
callers can write::
|
|
112
|
+
|
|
113
|
+
self.r.set(assert_prefixed(some_key), value)
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(key, str):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Redis key violates namespace policy (must be a str): {key!r}"
|
|
118
|
+
)
|
|
119
|
+
if not any(key.startswith(p + ":") for p in _RECOGNISED_PREFIXES):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Redis key violates namespace policy (must start with one "
|
|
122
|
+
f"of {sorted(_RECOGNISED_PREFIXES)}): {key!r}"
|
|
123
|
+
)
|
|
124
|
+
return key
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_reserved_super_name(name: str) -> bool:
|
|
128
|
+
"""Return True when *name* is reserved (cannot be a supertable name)."""
|
|
129
|
+
if not isinstance(name, str):
|
|
130
|
+
return False
|
|
131
|
+
return name.strip() in RESERVED_SUPER_NAMES
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --------------------------------------------------------------------------- #
|
|
135
|
+
# Per-organization system scope (registry, auth tokens, future system keys)
|
|
136
|
+
# --------------------------------------------------------------------------- #
|
|
137
|
+
|
|
138
|
+
def system_scope(org: str) -> str:
|
|
139
|
+
"""Return the system-scope prefix for one organization."""
|
|
140
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{SYSTEM_SCOPE}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def system_scope_pattern(org: str) -> str:
|
|
144
|
+
"""SCAN pattern for everything under one org's system scope."""
|
|
145
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{SYSTEM_SCOPE}:*"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --------------------------------------------------------------------------- #
|
|
149
|
+
# Service registry (per organization) — under the dataisland: prefix
|
|
150
|
+
# --------------------------------------------------------------------------- #
|
|
151
|
+
#
|
|
152
|
+
# Service-instance heartbeats are a *platform* concern, not a SuperTable
|
|
153
|
+
# concern. They live at:
|
|
154
|
+
#
|
|
155
|
+
# dataisland:{org}:registry:{service_type}:{host}:{pid}
|
|
156
|
+
#
|
|
157
|
+
# (No ``_system_`` segment — that segment exists only inside
|
|
158
|
+
# ``supertable:{org}:`` to avoid colliding with user-created supertable
|
|
159
|
+
# names. Under ``dataisland:`` there are no user supertables, so the
|
|
160
|
+
# extra segment is dropped.)
|
|
161
|
+
|
|
162
|
+
def registry(org: str, service_type: str, host: str, pid: int) -> str:
|
|
163
|
+
"""Per-organization service registry entry.
|
|
164
|
+
|
|
165
|
+
Example::
|
|
166
|
+
|
|
167
|
+
dataisland:kladna-soft:registry:api:host1:1234
|
|
168
|
+
"""
|
|
169
|
+
return f"{DATAISLAND_PREFIX}:{org}:registry:{service_type}:{host}:{pid}"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def registry_pattern_for_org(org: str) -> str:
|
|
173
|
+
"""SCAN pattern for all service-registry entries in one organization."""
|
|
174
|
+
return f"{DATAISLAND_PREFIX}:{org}:registry:*"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def registry_pattern() -> str:
|
|
178
|
+
"""SCAN pattern for service-registry entries across every organization.
|
|
179
|
+
|
|
180
|
+
Matches ``dataisland:*:registry:*`` — the cross-org pattern that
|
|
181
|
+
scanners use when no org is supplied (e.g. fleet-wide monitoring).
|
|
182
|
+
"""
|
|
183
|
+
return f"{DATAISLAND_PREFIX}:*:registry:*"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --------------------------------------------------------------------------- #
|
|
187
|
+
# App bootstrap (per application — Lighthouse, Gatekeeper, Studio, …)
|
|
188
|
+
# --------------------------------------------------------------------------- #
|
|
189
|
+
#
|
|
190
|
+
# The very first thing an app reads on boot is its master-MCP config.
|
|
191
|
+
# That happens before any org context exists, so the key sits at a
|
|
192
|
+
# single global location keyed only by app_name:
|
|
193
|
+
#
|
|
194
|
+
# dataisland:apps:{app_name}:master_mcp
|
|
195
|
+
#
|
|
196
|
+
# Written via the platform REST API (``POST /api/v1/apps/{app}/master-mcp``,
|
|
197
|
+
# admin-only) and read on every app boot.
|
|
198
|
+
|
|
199
|
+
def app_master_mcp(app_name: str) -> str:
|
|
200
|
+
"""The platform-side key holding an app's master-MCP coordinates."""
|
|
201
|
+
return f"{DATAISLAND_PREFIX}:apps:{app_name}:master_mcp"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# --------------------------------------------------------------------------- #
|
|
205
|
+
# Organization auth (login tokens) — also under _system_ for the same reason:
|
|
206
|
+
# "auth" must not collide with a possible user-created supertable called "auth".
|
|
207
|
+
# --------------------------------------------------------------------------- #
|
|
208
|
+
|
|
209
|
+
def auth_tokens(org: str) -> str:
|
|
210
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{SYSTEM_SCOPE}:auth:tokens"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --------------------------------------------------------------------------- #
|
|
214
|
+
# Organization scope (non-system)
|
|
215
|
+
# --------------------------------------------------------------------------- #
|
|
216
|
+
|
|
217
|
+
def share_doc(org: str, share_id: str) -> str:
|
|
218
|
+
return f"{SUPERTABLE_PREFIX}:{org}:shares:doc:{share_id}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def share_index(org: str) -> str:
|
|
222
|
+
return f"{SUPERTABLE_PREFIX}:{org}:shares:index"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def audit_stream(org: str) -> str:
|
|
226
|
+
return f"{SUPERTABLE_PREFIX}:{org}:audit:stream"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def audit_chain_head(org: str, instance_id: str) -> str:
|
|
230
|
+
return f"{SUPERTABLE_PREFIX}:{org}:audit:chain_head:{instance_id}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def audit_config(org: str) -> str:
|
|
234
|
+
"""Per-organization runtime audit configuration (enable toggle, sub-flags)."""
|
|
235
|
+
return f"{SUPERTABLE_PREFIX}:{org}:audit:config"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def spark_thrifts(org: str) -> str:
|
|
239
|
+
return f"{SUPERTABLE_PREFIX}:{org}:spark:thrifts"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def spark_plugs(org: str) -> str:
|
|
243
|
+
return f"{SUPERTABLE_PREFIX}:{org}:spark:plugs"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --------------------------------------------------------------------------- #
|
|
247
|
+
# SuperTable scope — meta
|
|
248
|
+
# --------------------------------------------------------------------------- #
|
|
249
|
+
|
|
250
|
+
def meta_root(org: str, sup: str) -> str:
|
|
251
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:root"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def meta_leaf(org: str, sup: str, simple: str) -> str:
|
|
255
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:leaf:{simple}"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def meta_leaf_pattern(org: str, sup: str) -> str:
|
|
259
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:leaf:*"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def meta_root_pattern_for_org(org: str) -> str:
|
|
263
|
+
return f"{SUPERTABLE_PREFIX}:{org}:*:meta:root"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def meta_mirrors(org: str, sup: str) -> str:
|
|
267
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:mirrors"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def meta_table_config(org: str, sup: str, simple: str) -> str:
|
|
271
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:table_config:{simple}"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# --------------------------------------------------------------------------- #
|
|
275
|
+
# SuperTable scope — engine config
|
|
276
|
+
# --------------------------------------------------------------------------- #
|
|
277
|
+
|
|
278
|
+
def config_engine(org: str, sup: str) -> str:
|
|
279
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:config:engine"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# --------------------------------------------------------------------------- #
|
|
283
|
+
# SuperTable scope — locks
|
|
284
|
+
# --------------------------------------------------------------------------- #
|
|
285
|
+
|
|
286
|
+
def lock_leaf(org: str, sup: str, simple: str) -> str:
|
|
287
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:lock:leaf:{simple}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def lock_stage(org: str, sup: str, stage_name: str) -> str:
|
|
291
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:lock:stage:{stage_name}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# --------------------------------------------------------------------------- #
|
|
295
|
+
# SuperTable scope — RBAC users
|
|
296
|
+
# --------------------------------------------------------------------------- #
|
|
297
|
+
|
|
298
|
+
def rbac_user_meta(org: str, sup: str) -> str:
|
|
299
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:users:meta"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def rbac_user_index(org: str, sup: str) -> str:
|
|
303
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:users:index"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def rbac_user_doc(org: str, sup: str, user_id: str) -> str:
|
|
307
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:users:doc:{user_id}"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def rbac_username_to_id(org: str, sup: str) -> str:
|
|
311
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:users:name_to_id"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --------------------------------------------------------------------------- #
|
|
315
|
+
# SuperTable scope — RBAC roles
|
|
316
|
+
# --------------------------------------------------------------------------- #
|
|
317
|
+
|
|
318
|
+
def rbac_role_meta(org: str, sup: str) -> str:
|
|
319
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:roles:meta"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def rbac_role_index(org: str, sup: str) -> str:
|
|
323
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:roles:index"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def rbac_role_doc(org: str, sup: str, role_id: str) -> str:
|
|
327
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:roles:doc:{role_id}"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def rbac_role_type_index(org: str, sup: str, role_type: str) -> str:
|
|
331
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:roles:type:{role_type}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def rbac_rolename_to_id(org: str, sup: str) -> str:
|
|
335
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:rbac:roles:name_to_id"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# --------------------------------------------------------------------------- #
|
|
339
|
+
# SuperTable scope — staging / pipes
|
|
340
|
+
# --------------------------------------------------------------------------- #
|
|
341
|
+
|
|
342
|
+
def staging(org: str, sup: str, staging_name: str) -> str:
|
|
343
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:{staging_name}"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def staging_index(org: str, sup: str) -> str:
|
|
347
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:meta"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def staging_pattern(org: str, sup: str) -> str:
|
|
351
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:*"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def staging_subkey_pattern(org: str, sup: str, staging_name: str) -> str:
|
|
355
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:{staging_name}:*"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def pipe(org: str, sup: str, staging_name: str, pipe_name: str) -> str:
|
|
359
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:{staging_name}:pipe:{pipe_name}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def pipe_index(org: str, sup: str, staging_name: str) -> str:
|
|
363
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:{staging_name}:pipe:meta"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def pipe_pattern(org: str, sup: str, staging_name: str) -> str:
|
|
367
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:meta:staging:{staging_name}:pipe:*"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# --------------------------------------------------------------------------- #
|
|
371
|
+
# SuperTable scope — schema / table_names / linked shares
|
|
372
|
+
# --------------------------------------------------------------------------- #
|
|
373
|
+
|
|
374
|
+
def schema(org: str, sup: str, simple: str) -> str:
|
|
375
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:schema:{simple}"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def table_names(org: str, sup: str) -> str:
|
|
379
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:table_names"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def linked_share_doc(org: str, sup: str, link_id: str) -> str:
|
|
383
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:linked_shares:doc:{link_id}"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def linked_share_index(org: str, sup: str) -> str:
|
|
387
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:linked_shares:index"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# --------------------------------------------------------------------------- #
|
|
391
|
+
# SuperTable scope — monitoring
|
|
392
|
+
# --------------------------------------------------------------------------- #
|
|
393
|
+
|
|
394
|
+
def monitor(org: str, sup: str, monitor_type: str) -> str:
|
|
395
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:monitor:{monitor_type}"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# --------------------------------------------------------------------------- #
|
|
399
|
+
# Wildcard / SCAN helpers used by deletes
|
|
400
|
+
# --------------------------------------------------------------------------- #
|
|
401
|
+
|
|
402
|
+
def super_table_pattern(org: str, sup: str) -> str:
|
|
403
|
+
"""Matches every key for one supertable (used by delete_super_table)."""
|
|
404
|
+
return f"{SUPERTABLE_PREFIX}:{org}:{sup}:*"
|
|
@@ -13,6 +13,7 @@ from supertable.rbac.user_manager import UserManager
|
|
|
13
13
|
from supertable.storage.storage_factory import get_storage
|
|
14
14
|
from supertable.storage.storage_interface import StorageInterface
|
|
15
15
|
from supertable.redis_catalog import RedisCatalog
|
|
16
|
+
from supertable.redis_keys import is_reserved_super_name, RESERVED_SUPER_NAMES
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class SuperTable:
|
|
@@ -21,9 +22,20 @@ class SuperTable:
|
|
|
21
22
|
- Ensures storage backend is available
|
|
22
23
|
- Ensures Redis meta:root exists (no file-based meta)
|
|
23
24
|
- Exposes helper to read heavy simple-table snapshots from MinIO/local via StorageInterface
|
|
25
|
+
|
|
26
|
+
Reserved supertable names (e.g. ``_system_``) are rejected up-front
|
|
27
|
+
so they can never collide with the org-level system scope where the
|
|
28
|
+
service registry, organization auth tokens, and other system-only
|
|
29
|
+
state live.
|
|
24
30
|
"""
|
|
25
31
|
|
|
26
32
|
def __init__(self, super_name: str, organization: str):
|
|
33
|
+
if is_reserved_super_name(super_name):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"SuperTable name {super_name!r} is reserved and cannot be created. "
|
|
36
|
+
f"Reserved names: {sorted(RESERVED_SUPER_NAMES)}"
|
|
37
|
+
)
|
|
38
|
+
|
|
27
39
|
self.identity = "super"
|
|
28
40
|
self.super_name = super_name
|
|
29
41
|
self.organization = organization
|