supertable 2.0.4__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.4/supertable.egg-info → supertable-2.0.5}/PKG-INFO +1 -1
- {supertable-2.0.4 → supertable-2.0.5}/pyproject.toml +1 -1
- {supertable-2.0.4 → supertable-2.0.5}/setup.py +1 -1
- {supertable-2.0.4 → supertable-2.0.5}/supertable/__init__.py +1 -1
- {supertable-2.0.4 → supertable-2.0.5}/supertable/redis_infra.py +24 -7
- supertable-2.0.5/supertable/redis_keys.py +404 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/super_table.py +12 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_redis_key_prefix.py +79 -7
- {supertable-2.0.4 → supertable-2.0.5/supertable.egg-info}/PKG-INFO +1 -1
- {supertable-2.0.4 → supertable-2.0.5}/supertable.egg-info/SOURCES.txt +0 -1
- supertable-2.0.4/supertable/redis_keys.py +0 -286
- supertable-2.0.4/supertable/service_registry.py +0 -253
- {supertable-2.0.4 → supertable-2.0.5}/LICENSE +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/README.md +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/requirements.txt +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/setup.cfg +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/admin.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/chain.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/consumers.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/crypto.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/events.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/export.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/logger.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/middleware.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/reader.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/retention.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/test_chain.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/test_crypto.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/test_emit.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/test_events.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/tests/test_retention.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/writer_parquet.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/audit/writer_redis.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/defaults.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/homedir.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/settings.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/tests/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/tests/test_defaults.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/tests/test_homedir.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/config/tests/test_settings.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/data_classes.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/data_reader.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/data_writer.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/__main__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/check_filter_builder.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/controller.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/data_writer_helpers.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/defaults.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/dummy_data.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/read_parquet_header.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s01_01_01_create_super_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s01_01_02_enable_mirroring_formats.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s01_02_create_roles.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s01_03_create_users.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_01_write_dummy_data.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_02_write_single_data.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_03_01_write_staging.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_03_02_create_pipe.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_04_01_write_monitoring_simple.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_04_02_write_monitoring_parallel.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s02_05_write_tombstone.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_01_read_data_error.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_02_01_read_super_data_ok.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_02_02_read_table_data_ok.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_03_read_meta.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_04_read_staging.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_06_01_read_roles.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_06_02_read_user.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_07_01_estimate_read.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_07_02_estimate_files.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s03_08_read_snapshot_history.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s04_01_03_delete_pipe.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s05_01_delete_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/quickstart/s05_02_delete_super_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/core.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/defaults.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/generate.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/load.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/demo/webshop/topup.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/data_estimator.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/duckdb_lite.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/duckdb_pro.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/engine_common.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/engine_enum.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/executor.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/plan_stats.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/spark_thrift.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/tests/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/tests/conftest.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/tests/test_dedup_read.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/engine/tests/test_engine.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/benchmarks/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/benchmarks/benchmark_locking.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/benchmarks/measure_lock_speed.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/benchmarks/measure_lock_time.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/file_lock.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/redis_lock.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/tests/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/tests/test_file_lock.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/locking/tests/test_redis_lock.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/logging.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/meta_reader.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/mirroring/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/mirroring/mirror_delta.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/mirroring/mirror_formats.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/mirroring/mirror_iceberg.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/mirroring/mirror_parquet.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/monitoring_writer.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/plan_extender.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/processing.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/query_plan_manager.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/access_control.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/filter_builder.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/permissions.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/role_manager.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/row_column_security.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/tests/test_filter_builder.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/tests/test_rbac.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/tests/test_rbac_per_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/rbac/user_manager.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/redis_catalog.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/redis_connector.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/simple_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/staging_area.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/azure_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/gcp_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/local_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/minio_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/s3_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/storage_factory.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/storage_interface.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/storage/tests/test_storage.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/super_pipe.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_align_to_schema_fix.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_data_reader.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_data_writer.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_data_writer_comprehensive.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_data_writer_tombstones.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_dedup_on_read_write.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_meta_reader.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_newer_than.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_process_delete_only.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_processing.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_query_sql.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_simple_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_small_file_compaction.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_super_table.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/tests/test_supertable_all.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/utils/__init__.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/utils/helper.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/utils/sql_parser.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/utils/tests/test_sql_parser_columns.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable/utils/timer.py +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable.egg-info/dependency_links.txt +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable.egg-info/entry_points.txt +0 -0
- {supertable-2.0.4 → supertable-2.0.5}/supertable.egg-info/requires.txt +0 -0
- {supertable-2.0.4 → 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.
|
|
@@ -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
|
|
@@ -38,8 +38,12 @@ PIPE = "pipe1"
|
|
|
38
38
|
def _all_helpers() -> list[tuple[str, str]]:
|
|
39
39
|
"""Return (helper_name, sample_key) pairs for every key formatter."""
|
|
40
40
|
return [
|
|
41
|
-
("
|
|
41
|
+
("system_scope", RK.system_scope(ORG)),
|
|
42
|
+
("system_scope_pattern", RK.system_scope_pattern(ORG)),
|
|
43
|
+
("registry", RK.registry(ORG, "api", "host", 1234)),
|
|
44
|
+
("registry_pattern_for_org", RK.registry_pattern_for_org(ORG)),
|
|
42
45
|
("registry_pattern", RK.registry_pattern()),
|
|
46
|
+
("app_master_mcp", RK.app_master_mcp("lighthouse")),
|
|
43
47
|
("auth_tokens", RK.auth_tokens(ORG)),
|
|
44
48
|
("share_doc", RK.share_doc(ORG, "share_1")),
|
|
45
49
|
("share_index", RK.share_index(ORG)),
|
|
@@ -83,24 +87,92 @@ def _all_helpers() -> list[tuple[str, str]]:
|
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
@pytest.mark.parametrize("name,key", _all_helpers())
|
|
86
|
-
def
|
|
90
|
+
def test_helpers_emit_recognised_prefix(name, key):
|
|
91
|
+
"""Every constructor must emit a key under a recognised top-level prefix.
|
|
92
|
+
|
|
93
|
+
Recognised: ``supertable:`` (SDK state) or ``dataisland:`` (platform
|
|
94
|
+
state — service registry + app bootstrap). Per-app prefixes
|
|
95
|
+
(``lighthouse:``, ``gatekeeper:``, …) are not built through this
|
|
96
|
+
module — they go via the MCP ``store_app_config`` tool.
|
|
97
|
+
"""
|
|
87
98
|
assert isinstance(key, str), f"{name} returned non-str: {key!r}"
|
|
88
|
-
assert
|
|
99
|
+
assert (
|
|
100
|
+
key.startswith("supertable:") or key.startswith("dataisland:")
|
|
101
|
+
), (
|
|
89
102
|
f"{name} returned {key!r} which violates the namespace policy "
|
|
90
|
-
f"(must start with 'supertable:')"
|
|
103
|
+
f"(must start with 'supertable:' or 'dataisland:')"
|
|
91
104
|
)
|
|
92
105
|
|
|
93
106
|
|
|
94
|
-
def
|
|
107
|
+
def test_assert_prefixed_accepts_supertable_key():
|
|
95
108
|
assert RK.assert_prefixed("supertable:org:foo") == "supertable:org:foo"
|
|
96
109
|
|
|
97
110
|
|
|
111
|
+
def test_assert_prefixed_accepts_dataisland_key():
|
|
112
|
+
assert RK.assert_prefixed("dataisland:org:foo") == "dataisland:org:foo"
|
|
113
|
+
|
|
114
|
+
|
|
98
115
|
def test_assert_prefixed_rejects_bare_root_keys():
|
|
99
|
-
for bad in ("monitor:org:sup:plans", "spark:org:thrifts", "registry:api"
|
|
116
|
+
for bad in ("monitor:org:sup:plans", "spark:org:thrifts", "registry:api",
|
|
117
|
+
"lighthouse:acme:config"): # per-app prefixes not built here
|
|
100
118
|
with pytest.raises(ValueError):
|
|
101
119
|
RK.assert_prefixed(bad)
|
|
102
120
|
|
|
103
121
|
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# 1b. Layout invariants
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def test_registry_key_lives_under_dataisland_prefix():
|
|
127
|
+
"""Service-registry keys belong to the platform layer, not SuperTable."""
|
|
128
|
+
key = RK.registry(ORG, "api", "host1", 1234)
|
|
129
|
+
assert key == f"dataisland:{ORG}:registry:api:host1:1234"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_app_master_mcp_lives_under_dataisland_prefix():
|
|
133
|
+
"""Bootstrap key for an app's master MCP — global, not org-scoped."""
|
|
134
|
+
key = RK.app_master_mcp("lighthouse")
|
|
135
|
+
assert key == "dataisland:apps:lighthouse:master_mcp"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_auth_tokens_still_lives_under_system_scope():
|
|
139
|
+
"""Org auth tokens stay under supertable:{org}:_system_:auth:tokens
|
|
140
|
+
(managed by the SuperTable SDK's RBAC / auth subsystem)."""
|
|
141
|
+
key = RK.auth_tokens(ORG)
|
|
142
|
+
assert key == f"supertable:{ORG}:_system_:auth:tokens"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_registry_pattern_targets_all_orgs():
|
|
146
|
+
"""The cross-org SCAN pattern reaches every org's registry."""
|
|
147
|
+
assert RK.registry_pattern() == "dataisland:*:registry:*"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_registry_pattern_for_org_is_scoped():
|
|
151
|
+
assert RK.registry_pattern_for_org(ORG) == f"dataisland:{ORG}:registry:*"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# 1c. Reserved supertable names
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def test_system_scope_name_is_reserved():
|
|
159
|
+
assert RK.is_reserved_super_name("_system_") is True
|
|
160
|
+
assert "_system_" in RK.RESERVED_SUPER_NAMES
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_arbitrary_supertable_name_is_not_reserved():
|
|
164
|
+
for name in ("orders", "customers", "_system", "system_", "auth"):
|
|
165
|
+
assert RK.is_reserved_super_name(name) is False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_super_table_refuses_reserved_name(monkeypatch):
|
|
169
|
+
"""``SuperTable(super_name='_system_')`` must raise before touching Redis."""
|
|
170
|
+
# Import lazily so the regression also covers fresh imports.
|
|
171
|
+
from supertable.super_table import SuperTable
|
|
172
|
+
with pytest.raises(ValueError, match="reserved"):
|
|
173
|
+
SuperTable(super_name="_system_", organization=ORG)
|
|
174
|
+
|
|
175
|
+
|
|
104
176
|
# ---------------------------------------------------------------------------
|
|
105
177
|
# 2. No raw f-string keys outside redis_keys.py / Lua / docs
|
|
106
178
|
# ---------------------------------------------------------------------------
|
|
@@ -109,7 +181,7 @@ def test_assert_prefixed_rejects_bare_root_keys():
|
|
|
109
181
|
# redis_keys.py. Note the search is restricted to source files in the
|
|
110
182
|
# library itself.
|
|
111
183
|
_FORBIDDEN_PATTERNS = re.compile(
|
|
112
|
-
r"""f["'](?:supertable|monitor|spark|registry|audit):"""
|
|
184
|
+
r"""f["'](?:supertable|dataisland|monitor|spark|registry|audit):"""
|
|
113
185
|
)
|
|
114
186
|
|
|
115
187
|
# Files / paths that are intentionally exempt from the scan:
|