databricks-tellr 0.3.13.dev4__tar.gz → 0.3.13.dev12__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.dev4 → databricks_tellr-0.3.13.dev12}/PKG-INFO +1 -1
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr/_templates/app.yaml.template +4 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr/deploy.py +35 -121
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr.egg-info/PKG-INFO +1 -1
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr.egg-info/SOURCES.txt +0 -1
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/pyproject.toml +1 -1
- databricks_tellr-0.3.13.dev4/databricks_tellr/identifiers.py +0 -37
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/README.md +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr/__init__.py +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr/_templates/requirements.txt.template +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr.egg-info/dependency_links.txt +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr.egg-info/requires.txt +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/databricks_tellr.egg-info/top_level.txt +0 -0
- {databricks_tellr-0.3.13.dev4 → databricks_tellr-0.3.13.dev12}/setup.cfg +0 -0
|
@@ -27,6 +27,8 @@ 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}"
|
|
30
32
|
- name: LAKEBASE_TYPE
|
|
31
33
|
value: "${LAKEBASE_TYPE}"
|
|
32
34
|
- name: LAKEBASE_PG_HOST
|
|
@@ -37,6 +39,8 @@ env:
|
|
|
37
39
|
value: "${LAKEBASE_ENDPOINT_NAME}"
|
|
38
40
|
- name: DATABRICKS_HOST
|
|
39
41
|
valueFrom: "system.databricks_host"
|
|
42
|
+
- name: DATABRICKS_TOKEN
|
|
43
|
+
valueFrom: "system.databricks_token"
|
|
40
44
|
# Opt-in to trust x-forwarded-email / x-forwarded-user on the MCP endpoint
|
|
41
45
|
# for app-to-app calls. Safe inside Databricks Apps because the proxy sets
|
|
42
46
|
# these headers itself and strips any caller-supplied versions. See
|
|
@@ -30,8 +30,6 @@ 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
|
-
|
|
35
33
|
# Autoscaling imports (Lakebase next-gen)
|
|
36
34
|
try:
|
|
37
35
|
from databricks.sdk.service.postgres import (
|
|
@@ -197,6 +195,7 @@ def create(
|
|
|
197
195
|
client: WorkspaceClient | None = None,
|
|
198
196
|
profile: str | None = None,
|
|
199
197
|
config_yaml_path: str | None = None,
|
|
198
|
+
encryption_key: str | None = None,
|
|
200
199
|
mlflow_tracing: dict[str, str] | None = None,
|
|
201
200
|
) -> dict[str, Any]:
|
|
202
201
|
"""Deploy Tellr to Databricks Apps.
|
|
@@ -226,6 +225,7 @@ def create(
|
|
|
226
225
|
client: External WorkspaceClient (optional)
|
|
227
226
|
profile: Databricks CLI profile name (optional)
|
|
228
227
|
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,6 +258,7 @@ 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,
|
|
261
262
|
mlflow_tracing=mlflow_tracing,
|
|
262
263
|
)
|
|
263
264
|
|
|
@@ -287,9 +288,8 @@ def update(
|
|
|
287
288
|
reset_database: If True, drop and recreate the schema (tables recreated on app startup)
|
|
288
289
|
client: External WorkspaceClient (optional)
|
|
289
290
|
profile: Databricks CLI profile name (optional)
|
|
290
|
-
encryption_key:
|
|
291
|
-
|
|
292
|
-
app.yaml. The key is no longer written to app.yaml.
|
|
291
|
+
encryption_key: Fernet key for Google OAuth encryption. If not provided, the
|
|
292
|
+
existing key is read from the deployed app.yaml to preserve encrypted data.
|
|
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,6 +333,7 @@ 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,
|
|
336
337
|
mlflow_tracing: dict[str, str] | None = None,
|
|
337
338
|
) -> dict[str, Any]:
|
|
338
339
|
"""Deploy Tellr to Databricks Apps with configurable seeding.
|
|
@@ -352,6 +353,7 @@ def _create_databricks(
|
|
|
352
353
|
profile: Databricks CLI profile name (optional)
|
|
353
354
|
config_yaml_path: Path to deployment config YAML (mutually exclusive with other args)
|
|
354
355
|
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.
|
|
355
357
|
mlflow_tracing: Optional overrides for UC tracing placeholders in ``app.yaml``.
|
|
356
358
|
|
|
357
359
|
Returns:
|
|
@@ -415,10 +417,11 @@ def _create_databricks(
|
|
|
415
417
|
lakebase_name,
|
|
416
418
|
schema_name,
|
|
417
419
|
seed_databricks_defaults=seed_databricks_defaults,
|
|
420
|
+
encryption_key=encryption_key,
|
|
418
421
|
lakebase_result=lakebase_result,
|
|
419
422
|
mlflow_tracing=mlflow_subs,
|
|
420
423
|
)
|
|
421
|
-
print(" Generated app.yaml")
|
|
424
|
+
print(" Generated app.yaml (with encryption key)")
|
|
422
425
|
|
|
423
426
|
print(f"Uploading to: {app_file_workspace_path}")
|
|
424
427
|
_upload_files(ws, staging, app_file_workspace_path)
|
|
@@ -504,9 +507,8 @@ def _update_databricks(
|
|
|
504
507
|
client: External WorkspaceClient (optional)
|
|
505
508
|
profile: Databricks CLI profile name (optional)
|
|
506
509
|
seed_databricks_defaults: If True, seed Databricks-specific content on startup
|
|
507
|
-
encryption_key:
|
|
508
|
-
the
|
|
509
|
-
app.yaml. The key is no longer written to app.yaml.
|
|
510
|
+
encryption_key: Fernet key for Google OAuth encryption. If not provided, reads
|
|
511
|
+
the existing key from the deployed app.yaml to preserve encrypted data.
|
|
510
512
|
mlflow_tracing: Optional overrides for UC tracing placeholders in ``app.yaml``.
|
|
511
513
|
|
|
512
514
|
Returns:
|
|
@@ -545,28 +547,6 @@ def _update_databricks(
|
|
|
545
547
|
print(f" Schema '{schema_name}' reset (tables will be recreated on app startup)")
|
|
546
548
|
print()
|
|
547
549
|
|
|
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
|
-
|
|
570
550
|
# Generate and upload updated files
|
|
571
551
|
with _staging_dir() as staging:
|
|
572
552
|
_write_requirements(staging, app_version)
|
|
@@ -575,6 +555,7 @@ def _update_databricks(
|
|
|
575
555
|
lakebase_name,
|
|
576
556
|
schema_name,
|
|
577
557
|
seed_databricks_defaults=seed_databricks_defaults,
|
|
558
|
+
encryption_key=encryption_key,
|
|
578
559
|
lakebase_result=lakebase_result,
|
|
579
560
|
mlflow_tracing=mlflow_subs,
|
|
580
561
|
)
|
|
@@ -752,96 +733,32 @@ def _check_breaking_migrations(
|
|
|
752
733
|
def _read_existing_encryption_key(
|
|
753
734
|
ws: WorkspaceClient, workspace_path: str
|
|
754
735
|
) -> str | None:
|
|
755
|
-
"""Read GOOGLE_OAUTH_ENCRYPTION_KEY from an existing deployed app.yaml.
|
|
736
|
+
"""Read the GOOGLE_OAUTH_ENCRYPTION_KEY from an existing deployed app.yaml.
|
|
756
737
|
|
|
757
|
-
|
|
758
|
-
|
|
738
|
+
This preserves the encryption key across updates so that previously encrypted
|
|
739
|
+
credentials and tokens remain decryptable.
|
|
759
740
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
CRITICAL-3 migration and let the new code generate a fresh key,
|
|
763
|
-
orphaning all existing ciphertext.
|
|
741
|
+
Returns:
|
|
742
|
+
The encryption key string, or None if not found.
|
|
764
743
|
"""
|
|
765
744
|
try:
|
|
766
745
|
resp = ws.workspace.download(f"{workspace_path}/app.yaml")
|
|
767
746
|
raw = resp.read() if hasattr(resp, "read") else resp
|
|
768
747
|
content = raw.decode("utf-8") if isinstance(raw, bytes) else str(raw)
|
|
748
|
+
|
|
769
749
|
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
|
|
770
756
|
except Exception as e:
|
|
771
|
-
|
|
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
|
|
757
|
+
logger.warning("Could not read existing encryption key from app.yaml: %s", e)
|
|
776
758
|
|
|
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
|
|
783
759
|
return None
|
|
784
760
|
|
|
785
761
|
|
|
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
|
-
|
|
845
762
|
def _load_deployment_config(config_yaml_path: str) -> dict[str, str]:
|
|
846
763
|
"""Load deployment settings from config/deployment.yaml-style files.
|
|
847
764
|
|
|
@@ -1360,19 +1277,19 @@ def _write_app_yaml(
|
|
|
1360
1277
|
lakebase_name: str,
|
|
1361
1278
|
schema_name: str,
|
|
1362
1279
|
seed_databricks_defaults: bool = False,
|
|
1280
|
+
encryption_key: str | None = None,
|
|
1363
1281
|
lakebase_result: dict[str, Any] | None = None,
|
|
1364
1282
|
mlflow_tracing: dict[str, str] | None = None,
|
|
1365
1283
|
) -> None:
|
|
1366
1284
|
"""Generate app.yaml with environment variables.
|
|
1367
1285
|
|
|
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
|
-
|
|
1371
1286
|
Args:
|
|
1372
1287
|
staging_dir: Directory to write the app.yaml file
|
|
1373
1288
|
lakebase_name: Lakebase instance name
|
|
1374
1289
|
schema_name: Schema name
|
|
1375
1290
|
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.
|
|
1376
1293
|
lakebase_result: Result dict from _get_or_create_lakebase() with type info.
|
|
1377
1294
|
mlflow_tracing: Resolved template keys for UC tracing (four entries). If
|
|
1378
1295
|
omitted, values are taken only from ``TELLR_DEPLOY_MLFLOW_*`` env vars.
|
|
@@ -1383,6 +1300,12 @@ def _write_app_yaml(
|
|
|
1383
1300
|
else:
|
|
1384
1301
|
init_call = "init_database()"
|
|
1385
1302
|
|
|
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
|
+
|
|
1386
1309
|
# Determine lakebase type info for env vars
|
|
1387
1310
|
lakebase_type = (lakebase_result or {}).get("type", "provisioned")
|
|
1388
1311
|
lakebase_pg_host = (lakebase_result or {}).get("host", "")
|
|
@@ -1397,6 +1320,7 @@ def _write_app_yaml(
|
|
|
1397
1320
|
LAKEBASE_INSTANCE=lakebase_name,
|
|
1398
1321
|
LAKEBASE_SCHEMA=schema_name,
|
|
1399
1322
|
INIT_DATABASE_CALL=init_call,
|
|
1323
|
+
GOOGLE_OAUTH_ENCRYPTION_KEY=encryption_key,
|
|
1400
1324
|
LAKEBASE_TYPE=lakebase_type,
|
|
1401
1325
|
LAKEBASE_PG_HOST=lakebase_pg_host,
|
|
1402
1326
|
LAKEBASE_PROJECT_ID=lakebase_project_id,
|
|
@@ -1643,9 +1567,6 @@ def _setup_database_schema(
|
|
|
1643
1567
|
print(" Warning: Could not get app client ID - schema setup skipped")
|
|
1644
1568
|
return
|
|
1645
1569
|
|
|
1646
|
-
validate_schema_name(schema_name)
|
|
1647
|
-
validate_client_id(client_id)
|
|
1648
|
-
|
|
1649
1570
|
conn, _ = _get_lakebase_connection(ws, lakebase_name, lakebase_result=lakebase_result)
|
|
1650
1571
|
|
|
1651
1572
|
try:
|
|
@@ -1676,10 +1597,6 @@ def _reset_schema(
|
|
|
1676
1597
|
"""
|
|
1677
1598
|
client_id = _get_app_client_id(app)
|
|
1678
1599
|
|
|
1679
|
-
validate_schema_name(schema_name)
|
|
1680
|
-
if client_id:
|
|
1681
|
-
validate_client_id(client_id)
|
|
1682
|
-
|
|
1683
1600
|
conn, _ = _get_lakebase_connection(ws, lakebase_name, lakebase_result=lakebase_result)
|
|
1684
1601
|
|
|
1685
1602
|
try:
|
|
@@ -1707,9 +1624,6 @@ def _grant_schema_permissions(cur: Any, schema_name: str, client_id: str) -> Non
|
|
|
1707
1624
|
|
|
1708
1625
|
This function only grants schema/table permissions — it does NOT create roles.
|
|
1709
1626
|
"""
|
|
1710
|
-
validate_schema_name(schema_name)
|
|
1711
|
-
validate_client_id(client_id)
|
|
1712
|
-
|
|
1713
1627
|
# Verify the role exists before granting
|
|
1714
1628
|
cur.execute(
|
|
1715
1629
|
"SELECT 1 FROM pg_roles WHERE rolname = %s", (client_id,)
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|