databricks-tellr 0.3.13.dev2__tar.gz → 0.3.13.dev4__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.
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/PKG-INFO +1 -1
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr/_templates/app.yaml.template +0 -4
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr/deploy.py +121 -35
- databricks_tellr-0.3.13.dev4/databricks_tellr/identifiers.py +37 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/PKG-INFO +1 -1
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/SOURCES.txt +1 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/pyproject.toml +1 -1
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/README.md +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr/__init__.py +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr/_templates/requirements.txt.template +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/dependency_links.txt +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/requires.txt +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/top_level.txt +0 -0
- {databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/setup.cfg +0 -0
|
@@ -27,8 +27,6 @@ env:
|
|
|
27
27
|
value: "${LAKEBASE_INSTANCE}"
|
|
28
28
|
- name: LAKEBASE_SCHEMA
|
|
29
29
|
value: "${LAKEBASE_SCHEMA}"
|
|
30
|
-
- name: GOOGLE_OAUTH_ENCRYPTION_KEY
|
|
31
|
-
value: "${GOOGLE_OAUTH_ENCRYPTION_KEY}"
|
|
32
30
|
- name: LAKEBASE_TYPE
|
|
33
31
|
value: "${LAKEBASE_TYPE}"
|
|
34
32
|
- name: LAKEBASE_PG_HOST
|
|
@@ -39,8 +37,6 @@ env:
|
|
|
39
37
|
value: "${LAKEBASE_ENDPOINT_NAME}"
|
|
40
38
|
- name: DATABRICKS_HOST
|
|
41
39
|
valueFrom: "system.databricks_host"
|
|
42
|
-
- name: DATABRICKS_TOKEN
|
|
43
|
-
valueFrom: "system.databricks_token"
|
|
44
40
|
# Opt-in to trust x-forwarded-email / x-forwarded-user on the MCP endpoint
|
|
45
41
|
# for app-to-app calls. Safe inside Databricks Apps because the proxy sets
|
|
46
42
|
# these headers itself and strips any caller-supplied versions. See
|
|
@@ -30,6 +30,8 @@ from databricks.sdk.service.apps import (
|
|
|
30
30
|
from databricks.sdk.service.database import DatabaseInstance
|
|
31
31
|
from databricks.sdk.service.workspace import ImportFormat
|
|
32
32
|
|
|
33
|
+
from databricks_tellr.identifiers import validate_client_id, validate_schema_name
|
|
34
|
+
|
|
33
35
|
# Autoscaling imports (Lakebase next-gen)
|
|
34
36
|
try:
|
|
35
37
|
from databricks.sdk.service.postgres import (
|
|
@@ -195,7 +197,6 @@ def create(
|
|
|
195
197
|
client: WorkspaceClient | None = None,
|
|
196
198
|
profile: str | None = None,
|
|
197
199
|
config_yaml_path: str | None = None,
|
|
198
|
-
encryption_key: str | None = None,
|
|
199
200
|
mlflow_tracing: dict[str, str] | None = None,
|
|
200
201
|
) -> dict[str, Any]:
|
|
201
202
|
"""Deploy Tellr to Databricks Apps.
|
|
@@ -225,7 +226,6 @@ def create(
|
|
|
225
226
|
client: External WorkspaceClient (optional)
|
|
226
227
|
profile: Databricks CLI profile name (optional)
|
|
227
228
|
config_yaml_path: Path to deployment config YAML (mutually exclusive with other args)
|
|
228
|
-
encryption_key: Fernet key for Google OAuth encryption. Auto-generated if not provided.
|
|
229
229
|
mlflow_tracing: Optional overrides for UC tracing env vars in generated ``app.yaml``.
|
|
230
230
|
Keys: ``MLFLOW_TRACING_SQL_WAREHOUSE_ID``, ``TELLR_MLFLOW_UC_CATALOG``,
|
|
231
231
|
``TELLR_MLFLOW_UC_SCHEMA``, ``TELLR_MLFLOW_UC_TABLE_PREFIX``. With
|
|
@@ -258,7 +258,6 @@ def create(
|
|
|
258
258
|
profile=profile,
|
|
259
259
|
config_yaml_path=config_yaml_path,
|
|
260
260
|
seed_databricks_defaults=False,
|
|
261
|
-
encryption_key=encryption_key,
|
|
262
261
|
mlflow_tracing=mlflow_tracing,
|
|
263
262
|
)
|
|
264
263
|
|
|
@@ -288,8 +287,9 @@ def update(
|
|
|
288
287
|
reset_database: If True, drop and recreate the schema (tables recreated on app startup)
|
|
289
288
|
client: External WorkspaceClient (optional)
|
|
290
289
|
profile: Databricks CLI profile name (optional)
|
|
291
|
-
encryption_key:
|
|
292
|
-
|
|
290
|
+
encryption_key: Override for the legacy Fernet key to relocate into
|
|
291
|
+
the encryption_keys table. Default: read from the deployed
|
|
292
|
+
app.yaml. The key is no longer written to app.yaml.
|
|
293
293
|
mlflow_tracing: Optional overrides for UC tracing env vars (same keys as ``create``).
|
|
294
294
|
Values from deployment YAML are not loaded on update; use this argument or
|
|
295
295
|
``TELLR_DEPLOY_MLFLOW_*`` environment variables.
|
|
@@ -333,7 +333,6 @@ def _create_databricks(
|
|
|
333
333
|
profile: str | None = None,
|
|
334
334
|
config_yaml_path: str | None = None,
|
|
335
335
|
seed_databricks_defaults: bool = True,
|
|
336
|
-
encryption_key: str | None = None,
|
|
337
336
|
mlflow_tracing: dict[str, str] | None = None,
|
|
338
337
|
) -> dict[str, Any]:
|
|
339
338
|
"""Deploy Tellr to Databricks Apps with configurable seeding.
|
|
@@ -353,7 +352,6 @@ def _create_databricks(
|
|
|
353
352
|
profile: Databricks CLI profile name (optional)
|
|
354
353
|
config_yaml_path: Path to deployment config YAML (mutually exclusive with other args)
|
|
355
354
|
seed_databricks_defaults: If True, seed Databricks-specific content on startup
|
|
356
|
-
encryption_key: Fernet key for Google OAuth encryption. Auto-generated if not provided.
|
|
357
355
|
mlflow_tracing: Optional overrides for UC tracing placeholders in ``app.yaml``.
|
|
358
356
|
|
|
359
357
|
Returns:
|
|
@@ -417,11 +415,10 @@ def _create_databricks(
|
|
|
417
415
|
lakebase_name,
|
|
418
416
|
schema_name,
|
|
419
417
|
seed_databricks_defaults=seed_databricks_defaults,
|
|
420
|
-
encryption_key=encryption_key,
|
|
421
418
|
lakebase_result=lakebase_result,
|
|
422
419
|
mlflow_tracing=mlflow_subs,
|
|
423
420
|
)
|
|
424
|
-
print(" Generated app.yaml
|
|
421
|
+
print(" Generated app.yaml")
|
|
425
422
|
|
|
426
423
|
print(f"Uploading to: {app_file_workspace_path}")
|
|
427
424
|
_upload_files(ws, staging, app_file_workspace_path)
|
|
@@ -507,8 +504,9 @@ def _update_databricks(
|
|
|
507
504
|
client: External WorkspaceClient (optional)
|
|
508
505
|
profile: Databricks CLI profile name (optional)
|
|
509
506
|
seed_databricks_defaults: If True, seed Databricks-specific content on startup
|
|
510
|
-
encryption_key:
|
|
511
|
-
the
|
|
507
|
+
encryption_key: Override for the legacy Fernet key to relocate into
|
|
508
|
+
the encryption_keys table. Default: read from the deployed
|
|
509
|
+
app.yaml. The key is no longer written to app.yaml.
|
|
512
510
|
mlflow_tracing: Optional overrides for UC tracing placeholders in ``app.yaml``.
|
|
513
511
|
|
|
514
512
|
Returns:
|
|
@@ -547,6 +545,28 @@ def _update_databricks(
|
|
|
547
545
|
print(f" Schema '{schema_name}' reset (tables will be recreated on app startup)")
|
|
548
546
|
print()
|
|
549
547
|
|
|
548
|
+
# CRITICAL-3 migration: relocate the legacy app.yaml key into the
|
|
549
|
+
# encryption_keys table BEFORE the new (keyless) app.yaml overwrites
|
|
550
|
+
# it. Runs at most once per install: after it succeeds, the deployed
|
|
551
|
+
# app.yaml has no key entry and encryption_key is None on re-runs.
|
|
552
|
+
if encryption_key:
|
|
553
|
+
print("Relocating encryption key into Lakebase (encryption_keys)...")
|
|
554
|
+
app_for_grant = ws.apps.get(name=app_name)
|
|
555
|
+
grant_client_id = _get_app_client_id(app_for_grant)
|
|
556
|
+
if not grant_client_id:
|
|
557
|
+
print(" Warning: no app client ID — table grant will be skipped")
|
|
558
|
+
mig_conn, _ = _get_lakebase_connection(
|
|
559
|
+
ws, lakebase_name, lakebase_result=lakebase_result
|
|
560
|
+
)
|
|
561
|
+
try:
|
|
562
|
+
with mig_conn.cursor() as cur:
|
|
563
|
+
_migrate_encryption_key_to_lakebase(
|
|
564
|
+
cur, schema_name, grant_client_id, encryption_key
|
|
565
|
+
)
|
|
566
|
+
finally:
|
|
567
|
+
mig_conn.close()
|
|
568
|
+
print(" Key relocated (relocate, not rotate — no re-encryption)")
|
|
569
|
+
|
|
550
570
|
# Generate and upload updated files
|
|
551
571
|
with _staging_dir() as staging:
|
|
552
572
|
_write_requirements(staging, app_version)
|
|
@@ -555,7 +575,6 @@ def _update_databricks(
|
|
|
555
575
|
lakebase_name,
|
|
556
576
|
schema_name,
|
|
557
577
|
seed_databricks_defaults=seed_databricks_defaults,
|
|
558
|
-
encryption_key=encryption_key,
|
|
559
578
|
lakebase_result=lakebase_result,
|
|
560
579
|
mlflow_tracing=mlflow_subs,
|
|
561
580
|
)
|
|
@@ -733,32 +752,96 @@ def _check_breaking_migrations(
|
|
|
733
752
|
def _read_existing_encryption_key(
|
|
734
753
|
ws: WorkspaceClient, workspace_path: str
|
|
735
754
|
) -> str | None:
|
|
736
|
-
"""Read
|
|
755
|
+
"""Read GOOGLE_OAUTH_ENCRYPTION_KEY from an existing deployed app.yaml.
|
|
737
756
|
|
|
738
|
-
|
|
739
|
-
|
|
757
|
+
Returns the key string, or None when app.yaml is readable but carries no
|
|
758
|
+
key entry (already migrated, or a fresh-era install).
|
|
740
759
|
|
|
741
|
-
|
|
742
|
-
|
|
760
|
+
Raises DeploymentError when app.yaml cannot be downloaded or parsed:
|
|
761
|
+
silently treating an unreadable app.yaml as "no key" would skip the
|
|
762
|
+
CRITICAL-3 migration and let the new code generate a fresh key,
|
|
763
|
+
orphaning all existing ciphertext.
|
|
743
764
|
"""
|
|
744
765
|
try:
|
|
745
766
|
resp = ws.workspace.download(f"{workspace_path}/app.yaml")
|
|
746
767
|
raw = resp.read() if hasattr(resp, "read") else resp
|
|
747
768
|
content = raw.decode("utf-8") if isinstance(raw, bytes) else str(raw)
|
|
748
|
-
|
|
749
769
|
existing = yaml.safe_load(content)
|
|
750
|
-
for env_entry in existing.get("env", []):
|
|
751
|
-
if env_entry.get("name") == "GOOGLE_OAUTH_ENCRYPTION_KEY":
|
|
752
|
-
key = env_entry.get("value")
|
|
753
|
-
if key:
|
|
754
|
-
print(" Preserved existing encryption key from deployed app.yaml")
|
|
755
|
-
return key
|
|
756
770
|
except Exception as e:
|
|
757
|
-
|
|
771
|
+
raise DeploymentError(
|
|
772
|
+
f"Could not read the deployed app.yaml at {workspace_path}/app.yaml "
|
|
773
|
+
f"({e}). Aborting: the update must know whether a legacy encryption "
|
|
774
|
+
f"key needs relocating before it overwrites app.yaml."
|
|
775
|
+
) from e
|
|
758
776
|
|
|
777
|
+
for env_entry in (existing or {}).get("env", []):
|
|
778
|
+
if env_entry.get("name") == "GOOGLE_OAUTH_ENCRYPTION_KEY":
|
|
779
|
+
key = env_entry.get("value")
|
|
780
|
+
if key:
|
|
781
|
+
print(" Found legacy encryption key in deployed app.yaml")
|
|
782
|
+
return key
|
|
759
783
|
return None
|
|
760
784
|
|
|
761
785
|
|
|
786
|
+
def _migrate_encryption_key_to_lakebase(
|
|
787
|
+
cur: Any,
|
|
788
|
+
schema_name: str,
|
|
789
|
+
client_id: str | None,
|
|
790
|
+
encryption_key: str,
|
|
791
|
+
) -> None:
|
|
792
|
+
"""Relocate the legacy app.yaml Fernet key into <schema>.encryption_keys.
|
|
793
|
+
|
|
794
|
+
SDR-4437 CRITICAL-3, deploy-time migration. Relocate, not rotate: no
|
|
795
|
+
re-encryption of existing rows. Idempotent: re-running with the same key
|
|
796
|
+
is a no-op. A *different* pre-existing key is a hard error — silently
|
|
797
|
+
keeping either key would orphan the ciphertext encrypted under the other.
|
|
798
|
+
|
|
799
|
+
The GRANT is unconditional: the original deployer's ALTER DEFAULT
|
|
800
|
+
PRIVILEGES attaches to the creating role, so when a different identity
|
|
801
|
+
runs the upgrade this explicit grant is the only one that applies.
|
|
802
|
+
INSERT is included so the app's boot seed (insert-when-missing branch)
|
|
803
|
+
can never be privilege-blocked.
|
|
804
|
+
|
|
805
|
+
Takes an open cursor so callers control the endpoint (prod branch,
|
|
806
|
+
devloop fork, ...). Caller owns the connection lifecycle.
|
|
807
|
+
"""
|
|
808
|
+
validate_schema_name(schema_name)
|
|
809
|
+
if client_id is not None:
|
|
810
|
+
validate_client_id(client_id)
|
|
811
|
+
|
|
812
|
+
# Matches the SQLAlchemy model (src/database/models/encryption_key.py):
|
|
813
|
+
# id INTEGER PK (no autoincrement), key_value TEXT, created_at TIMESTAMP.
|
|
814
|
+
cur.execute(
|
|
815
|
+
f'CREATE TABLE IF NOT EXISTS "{schema_name}".encryption_keys ('
|
|
816
|
+
"id INTEGER PRIMARY KEY, "
|
|
817
|
+
"key_value TEXT NOT NULL, "
|
|
818
|
+
"created_at TIMESTAMP NOT NULL)"
|
|
819
|
+
)
|
|
820
|
+
cur.execute(
|
|
821
|
+
f'INSERT INTO "{schema_name}".encryption_keys (id, key_value, created_at) '
|
|
822
|
+
"VALUES (1, %s, CURRENT_TIMESTAMP) ON CONFLICT (id) DO NOTHING",
|
|
823
|
+
(encryption_key,),
|
|
824
|
+
)
|
|
825
|
+
cur.execute(
|
|
826
|
+
f'SELECT key_value FROM "{schema_name}".encryption_keys WHERE id = 1'
|
|
827
|
+
)
|
|
828
|
+
row = cur.fetchone()
|
|
829
|
+
if not row or row[0] != encryption_key:
|
|
830
|
+
raise DeploymentError(
|
|
831
|
+
"encryption_keys already holds a DIFFERENT key than the deployed "
|
|
832
|
+
"app.yaml. Refusing to continue: proceeding would orphan ciphertext "
|
|
833
|
+
"encrypted under one of the two keys. Determine which key decrypts "
|
|
834
|
+
"the existing google_oauth_tokens/google_global_credentials rows, "
|
|
835
|
+
"fix the encryption_keys row (or the app.yaml) to match, then "
|
|
836
|
+
"re-run the update. See SDR-4437 PR-3 design."
|
|
837
|
+
)
|
|
838
|
+
if client_id:
|
|
839
|
+
cur.execute(
|
|
840
|
+
f'GRANT SELECT, INSERT ON "{schema_name}".encryption_keys '
|
|
841
|
+
f'TO "{client_id}"'
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
|
|
762
845
|
def _load_deployment_config(config_yaml_path: str) -> dict[str, str]:
|
|
763
846
|
"""Load deployment settings from config/deployment.yaml-style files.
|
|
764
847
|
|
|
@@ -1277,19 +1360,19 @@ def _write_app_yaml(
|
|
|
1277
1360
|
lakebase_name: str,
|
|
1278
1361
|
schema_name: str,
|
|
1279
1362
|
seed_databricks_defaults: bool = False,
|
|
1280
|
-
encryption_key: str | None = None,
|
|
1281
1363
|
lakebase_result: dict[str, Any] | None = None,
|
|
1282
1364
|
mlflow_tracing: dict[str, str] | None = None,
|
|
1283
1365
|
) -> None:
|
|
1284
1366
|
"""Generate app.yaml with environment variables.
|
|
1285
1367
|
|
|
1368
|
+
The Fernet encryption key is deliberately NOT written here (SDR-4437
|
|
1369
|
+
CRITICAL-3): the app reads/seeds it from the encryption_keys table.
|
|
1370
|
+
|
|
1286
1371
|
Args:
|
|
1287
1372
|
staging_dir: Directory to write the app.yaml file
|
|
1288
1373
|
lakebase_name: Lakebase instance name
|
|
1289
1374
|
schema_name: Schema name
|
|
1290
1375
|
seed_databricks_defaults: If True, include Databricks-specific content seeding
|
|
1291
|
-
encryption_key: Fernet encryption key for Google OAuth credentials/tokens.
|
|
1292
|
-
Auto-generated if not provided.
|
|
1293
1376
|
lakebase_result: Result dict from _get_or_create_lakebase() with type info.
|
|
1294
1377
|
mlflow_tracing: Resolved template keys for UC tracing (four entries). If
|
|
1295
1378
|
omitted, values are taken only from ``TELLR_DEPLOY_MLFLOW_*`` env vars.
|
|
@@ -1300,12 +1383,6 @@ def _write_app_yaml(
|
|
|
1300
1383
|
else:
|
|
1301
1384
|
init_call = "init_database()"
|
|
1302
1385
|
|
|
1303
|
-
if not encryption_key:
|
|
1304
|
-
from cryptography.fernet import Fernet
|
|
1305
|
-
|
|
1306
|
-
encryption_key = Fernet.generate_key().decode()
|
|
1307
|
-
logger.info("Auto-generated GOOGLE_OAUTH_ENCRYPTION_KEY for deployment")
|
|
1308
|
-
|
|
1309
1386
|
# Determine lakebase type info for env vars
|
|
1310
1387
|
lakebase_type = (lakebase_result or {}).get("type", "provisioned")
|
|
1311
1388
|
lakebase_pg_host = (lakebase_result or {}).get("host", "")
|
|
@@ -1320,7 +1397,6 @@ def _write_app_yaml(
|
|
|
1320
1397
|
LAKEBASE_INSTANCE=lakebase_name,
|
|
1321
1398
|
LAKEBASE_SCHEMA=schema_name,
|
|
1322
1399
|
INIT_DATABASE_CALL=init_call,
|
|
1323
|
-
GOOGLE_OAUTH_ENCRYPTION_KEY=encryption_key,
|
|
1324
1400
|
LAKEBASE_TYPE=lakebase_type,
|
|
1325
1401
|
LAKEBASE_PG_HOST=lakebase_pg_host,
|
|
1326
1402
|
LAKEBASE_PROJECT_ID=lakebase_project_id,
|
|
@@ -1567,6 +1643,9 @@ def _setup_database_schema(
|
|
|
1567
1643
|
print(" Warning: Could not get app client ID - schema setup skipped")
|
|
1568
1644
|
return
|
|
1569
1645
|
|
|
1646
|
+
validate_schema_name(schema_name)
|
|
1647
|
+
validate_client_id(client_id)
|
|
1648
|
+
|
|
1570
1649
|
conn, _ = _get_lakebase_connection(ws, lakebase_name, lakebase_result=lakebase_result)
|
|
1571
1650
|
|
|
1572
1651
|
try:
|
|
@@ -1597,6 +1676,10 @@ def _reset_schema(
|
|
|
1597
1676
|
"""
|
|
1598
1677
|
client_id = _get_app_client_id(app)
|
|
1599
1678
|
|
|
1679
|
+
validate_schema_name(schema_name)
|
|
1680
|
+
if client_id:
|
|
1681
|
+
validate_client_id(client_id)
|
|
1682
|
+
|
|
1600
1683
|
conn, _ = _get_lakebase_connection(ws, lakebase_name, lakebase_result=lakebase_result)
|
|
1601
1684
|
|
|
1602
1685
|
try:
|
|
@@ -1624,6 +1707,9 @@ def _grant_schema_permissions(cur: Any, schema_name: str, client_id: str) -> Non
|
|
|
1624
1707
|
|
|
1625
1708
|
This function only grants schema/table permissions — it does NOT create roles.
|
|
1626
1709
|
"""
|
|
1710
|
+
validate_schema_name(schema_name)
|
|
1711
|
+
validate_client_id(client_id)
|
|
1712
|
+
|
|
1627
1713
|
# Verify the role exists before granting
|
|
1628
1714
|
cur.execute(
|
|
1629
1715
|
"SELECT 1 FROM pg_roles WHERE rolname = %s", (client_id,)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Validation for identifiers interpolated into Postgres DDL (SDR-4437 MEDIUM-5).
|
|
2
|
+
|
|
3
|
+
Both inputs are config/platform-derived today (schema_name from deploy
|
|
4
|
+
config, client_id from the app SP), so this is hardening against future
|
|
5
|
+
user-derived values, not a live injection.
|
|
6
|
+
|
|
7
|
+
Lives only in this (deploy-tool) distribution: the app distribution has no
|
|
8
|
+
DDL-identifier interpolation site once the dead setup_lakebase_schema is
|
|
9
|
+
removed, so there is no counterpart to keep in sync.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
_SCHEMA_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
15
|
+
# UUID (App SP client id) OR all-digits (str(service_principal_id) fallback
|
|
16
|
+
# in _get_app_client_id). Both are injection-safe charsets.
|
|
17
|
+
_CLIENT_ID_RE = re.compile(
|
|
18
|
+
r"^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}"
|
|
19
|
+
r"-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9]+)$"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_schema_name(schema: str) -> str:
|
|
24
|
+
"""Return *schema* if it is a safe Postgres schema identifier; else raise."""
|
|
25
|
+
if not isinstance(schema, str) or not _SCHEMA_RE.match(schema):
|
|
26
|
+
raise ValueError(f"Invalid Postgres schema name: {schema!r}")
|
|
27
|
+
return schema
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_client_id(client_id: str) -> str:
|
|
31
|
+
"""Return *client_id* if it is a UUID or numeric SP id; else raise."""
|
|
32
|
+
if not isinstance(client_id, str) or not _CLIENT_ID_RE.match(client_id):
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Invalid service-principal client id (expected UUID or numeric id): "
|
|
35
|
+
f"{client_id!r}"
|
|
36
|
+
)
|
|
37
|
+
return client_id
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr-0.3.13.dev2 → databricks_tellr-0.3.13.dev4}/databricks_tellr.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|