jusi-postgres 0.2.0__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.
@@ -0,0 +1,25 @@
1
+ .DS_Store
2
+
3
+ # Local Python environments
4
+ .venv/
5
+ venv/
6
+ env/
7
+
8
+ # Python bytecode and test caches
9
+ __pycache__/
10
+ *.py[cod]
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .coverage
15
+ htmlcov/
16
+ pytest-of-*/
17
+
18
+ # Packaging/build outputs
19
+ *.egg-info/
20
+ build/
21
+ dist/
22
+
23
+ # Local runtime scratch
24
+ .tmp/
25
+ *.log
@@ -0,0 +1,6 @@
1
+ FROM postgres:16-alpine
2
+
3
+ LABEL org.opencontainers.image.title="jusi-postgres-dev"
4
+ LABEL org.opencontainers.image.description="Local PostgreSQL fixture for jusi-postgres development"
5
+
6
+ COPY docker/postgres/initdb.d/ /docker-entrypoint-initdb.d/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 notawhaleble
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: jusi-postgres
3
+ Version: 0.2.0
4
+ Summary: PostgreSQL provider plugin for Jusi SQL
5
+ Author: Jusi contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 notawhaleble
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: jusi-sql<0.3,>=0.2
30
+ Requires-Dist: jusi<2,>=1.0
31
+ Requires-Dist: psycopg[binary]>=3.1
32
+ Requires-Dist: visidata<4,>=3
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest<9,>=8; extra == 'test'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # jusi-postgres
38
+
39
+ `jusi-postgres` is the PostgreSQL exact-provider plugin for Jusi 1.0 and the
40
+ shared `jusi-sql` family. It exposes the `postgres`/`postgresql` provider for
41
+ `%%sql` cells. The family owns target selection, magic dispatch, completion
42
+ ranges, metadata caching, operation routing, and common VisiData SQL commands.
43
+
44
+ Example session config:
45
+
46
+ ```toml
47
+ [sql.targets.analytics]
48
+ provider = "postgres"
49
+ host = "localhost"
50
+ port = 5432
51
+ dbname = "analytics"
52
+ user = "me"
53
+ initial_fetch = 100
54
+ ```
55
+
56
+ All psycopg connection options may be provided in the target config. The
57
+ additional `krb5ccname` option points at a custom Kerberos credential cache and
58
+ is passed to libpq through `KRB5CCNAME` while connecting.
59
+
60
+ Metadata for SQL completion is collected asynchronously on a separate
61
+ connection. Large catalogs should be filtered or disabled:
62
+
63
+ ```toml
64
+ [sql.targets.analytics]
65
+ provider = "postgres"
66
+ host = "db.example.com"
67
+ dbname = "analytics"
68
+ user = "me"
69
+ metadata_schemas = ["public", "analytics"]
70
+ metadata_max_rows = 1000000
71
+ collect_metadata = true
72
+ ```
73
+
74
+ Use `collect_metadata = false` to skip completion metadata for a target. If
75
+ collection crosses `metadata_max_rows`, it stops and warns that schema
76
+ filtering is required. Provider options belong in the target configuration;
77
+ the shared Jusi 1.0 `%%sql` syntax accepts one target alias.
78
+
79
+ VisiData mappings:
80
+
81
+ - `1` to `9`: fetch that many more rows for the active result sheet.
82
+ - `gf`: prompt for a row count to fetch; `0` fetches the rest of the cursor.
83
+ - `gc`: commit the current connection.
84
+ - `gr`: roll back the current connection.
85
+ - `gb`: write the selected raw cell value to a temporary file and open it with
86
+ VisiData. The command prompts for an optional extension; an empty extension
87
+ lets VisiData infer the file type from content and filename.
88
+
89
+ Follow-up cells reuse the same VisiData application, PostgreSQL connection,
90
+ transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
91
+ that connection. Closing the client closes its cursors, metadata connection,
92
+ main connection, private control socket, and staged launch data.
93
+
94
+ ## Local development database
95
+
96
+ Start a disposable PostgreSQL fixture:
97
+
98
+ ```sh
99
+ scripts/run-postgres.sh
100
+ ```
101
+
102
+ The script builds `jusi-postgres-dev`, runs a local container on
103
+ `127.0.0.1:55432`, waits for readiness, and prints this config:
104
+
105
+ ```toml
106
+ [sql.targets.local_postgres]
107
+ provider = "postgres"
108
+ host = "127.0.0.1"
109
+ port = 55432
110
+ dbname = "jusi"
111
+ user = "jusi"
112
+ password = "jusi"
113
+ initial_fetch = 25
114
+ ```
115
+
116
+ The fixture creates `demo.accounts`, `demo.events`, `demo.account_summary`,
117
+ `demo.account_event_count(bigint)`, and `demo.blob_files` for result browsing,
118
+ completion checks, and raw bytea/blob handling.
119
+
120
+ For an already-running development container, install or refresh the blob table:
121
+
122
+ ```sh
123
+ scripts/install-blob-fixture.sh
124
+ ```
125
+
126
+ The source zip fixture is stored at `fixtures/test-blob.zip`.
@@ -0,0 +1,90 @@
1
+ # jusi-postgres
2
+
3
+ `jusi-postgres` is the PostgreSQL exact-provider plugin for Jusi 1.0 and the
4
+ shared `jusi-sql` family. It exposes the `postgres`/`postgresql` provider for
5
+ `%%sql` cells. The family owns target selection, magic dispatch, completion
6
+ ranges, metadata caching, operation routing, and common VisiData SQL commands.
7
+
8
+ Example session config:
9
+
10
+ ```toml
11
+ [sql.targets.analytics]
12
+ provider = "postgres"
13
+ host = "localhost"
14
+ port = 5432
15
+ dbname = "analytics"
16
+ user = "me"
17
+ initial_fetch = 100
18
+ ```
19
+
20
+ All psycopg connection options may be provided in the target config. The
21
+ additional `krb5ccname` option points at a custom Kerberos credential cache and
22
+ is passed to libpq through `KRB5CCNAME` while connecting.
23
+
24
+ Metadata for SQL completion is collected asynchronously on a separate
25
+ connection. Large catalogs should be filtered or disabled:
26
+
27
+ ```toml
28
+ [sql.targets.analytics]
29
+ provider = "postgres"
30
+ host = "db.example.com"
31
+ dbname = "analytics"
32
+ user = "me"
33
+ metadata_schemas = ["public", "analytics"]
34
+ metadata_max_rows = 1000000
35
+ collect_metadata = true
36
+ ```
37
+
38
+ Use `collect_metadata = false` to skip completion metadata for a target. If
39
+ collection crosses `metadata_max_rows`, it stops and warns that schema
40
+ filtering is required. Provider options belong in the target configuration;
41
+ the shared Jusi 1.0 `%%sql` syntax accepts one target alias.
42
+
43
+ VisiData mappings:
44
+
45
+ - `1` to `9`: fetch that many more rows for the active result sheet.
46
+ - `gf`: prompt for a row count to fetch; `0` fetches the rest of the cursor.
47
+ - `gc`: commit the current connection.
48
+ - `gr`: roll back the current connection.
49
+ - `gb`: write the selected raw cell value to a temporary file and open it with
50
+ VisiData. The command prompts for an optional extension; an empty extension
51
+ lets VisiData infer the file type from content and filename.
52
+
53
+ Follow-up cells reuse the same VisiData application, PostgreSQL connection,
54
+ transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
55
+ that connection. Closing the client closes its cursors, metadata connection,
56
+ main connection, private control socket, and staged launch data.
57
+
58
+ ## Local development database
59
+
60
+ Start a disposable PostgreSQL fixture:
61
+
62
+ ```sh
63
+ scripts/run-postgres.sh
64
+ ```
65
+
66
+ The script builds `jusi-postgres-dev`, runs a local container on
67
+ `127.0.0.1:55432`, waits for readiness, and prints this config:
68
+
69
+ ```toml
70
+ [sql.targets.local_postgres]
71
+ provider = "postgres"
72
+ host = "127.0.0.1"
73
+ port = 55432
74
+ dbname = "jusi"
75
+ user = "jusi"
76
+ password = "jusi"
77
+ initial_fetch = 25
78
+ ```
79
+
80
+ The fixture creates `demo.accounts`, `demo.events`, `demo.account_summary`,
81
+ `demo.account_event_count(bigint)`, and `demo.blob_files` for result browsing,
82
+ completion checks, and raw bytea/blob handling.
83
+
84
+ For an already-running development container, install or refresh the blob table:
85
+
86
+ ```sh
87
+ scripts/install-blob-fixture.sh
88
+ ```
89
+
90
+ The source zip fixture is stored at `fixtures/test-blob.zip`.
@@ -0,0 +1,52 @@
1
+ CREATE SCHEMA IF NOT EXISTS demo;
2
+
3
+ CREATE TABLE IF NOT EXISTS demo.accounts (
4
+ id bigserial PRIMARY KEY,
5
+ email text NOT NULL UNIQUE,
6
+ display_name text NOT NULL,
7
+ active boolean NOT NULL DEFAULT true,
8
+ created_at timestamptz NOT NULL DEFAULT now()
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS demo.events (
12
+ id bigserial PRIMARY KEY,
13
+ account_id bigint NOT NULL REFERENCES demo.accounts(id),
14
+ event_type text NOT NULL,
15
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
16
+ created_at timestamptz NOT NULL DEFAULT now()
17
+ );
18
+
19
+ INSERT INTO demo.accounts (email, display_name, active)
20
+ VALUES
21
+ ('ada@example.test', 'Ada Lovelace', true),
22
+ ('grace@example.test', 'Grace Hopper', true),
23
+ ('alan@example.test', 'Alan Turing', false)
24
+ ON CONFLICT (email) DO NOTHING;
25
+
26
+ INSERT INTO demo.events (account_id, event_type, payload)
27
+ SELECT account_id, event_type, payload::jsonb
28
+ FROM (
29
+ VALUES
30
+ (1, 'login', '{"ip": "127.0.0.1"}'),
31
+ (1, 'query', '{"rows": 42}'),
32
+ (2, 'login', '{"ip": "127.0.0.2"}'),
33
+ (3, 'disabled_login', '{"reason": "inactive"}')
34
+ ) AS seed(account_id, event_type, payload)
35
+ WHERE NOT EXISTS (SELECT 1 FROM demo.events);
36
+
37
+ CREATE OR REPLACE FUNCTION demo.account_event_count(account_id bigint)
38
+ RETURNS bigint
39
+ LANGUAGE sql
40
+ STABLE
41
+ AS $$
42
+ SELECT count(*) FROM demo.events e WHERE e.account_id = $1
43
+ $$;
44
+
45
+ CREATE OR REPLACE VIEW demo.account_summary AS
46
+ SELECT
47
+ a.id,
48
+ a.email,
49
+ a.display_name,
50
+ a.active,
51
+ demo.account_event_count(a.id) AS event_count
52
+ FROM demo.accounts a;
@@ -0,0 +1,20 @@
1
+ CREATE TABLE IF NOT EXISTS demo.blob_files (
2
+ id bigserial PRIMARY KEY,
3
+ filename text NOT NULL UNIQUE,
4
+ content_type text NOT NULL,
5
+ payload bytea NOT NULL,
6
+ created_at timestamptz NOT NULL DEFAULT now()
7
+ );
8
+
9
+ INSERT INTO demo.blob_files (filename, content_type, payload)
10
+ VALUES (
11
+ 'test-blob.zip',
12
+ 'application/zip',
13
+ decode(
14
+ '504b03040a0000000000231ab65c2909ab311b0000001b0000000f001c00626c6f622d726561646d652e747874555409000382a00f6a82a00f6a75780b000104f501000004140000004a75736920506f73746772657320626c6f6220666978747572650a504b01021e030a0000000000231ab65c2909ab311b0000001b0000000f0018000000000001000000a48100000000626c6f622d726561646d652e747874555405000382a00f6a75780b000104f50100000414000000504b0506000000000100010055000000640000000000',
15
+ 'hex'
16
+ )
17
+ )
18
+ ON CONFLICT (filename) DO UPDATE
19
+ SET content_type = EXCLUDED.content_type,
20
+ payload = EXCLUDED.payload;
@@ -0,0 +1,8 @@
1
+ [sql.targets.local_postgres]
2
+ provider = "postgres"
3
+ host = "127.0.0.1"
4
+ port = 55432
5
+ dbname = "jusi"
6
+ user = "jusi"
7
+ password = "jusi"
8
+ initial_fetch = 25
@@ -0,0 +1 @@
1
+ Jusi Postgres blob fixture
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jusi-postgres"
7
+ version = "0.2.0"
8
+ description = "PostgreSQL provider plugin for Jusi SQL"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "Jusi contributors" }
14
+ ]
15
+ dependencies = [
16
+ "jusi>=1.0,<2",
17
+ "jusi-sql>=0.2,<0.3",
18
+ "psycopg[binary]>=3.1",
19
+ "visidata>=3,<4",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = ["pytest>=8,<9"]
24
+
25
+ [project.entry-points."jusi.plugins.v1"]
26
+ postgres = "jusi_postgres.catalog:catalog_entry"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/jusi_postgres"]
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src"]
33
+ testpaths = ["tests"]
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ CONTAINER_NAME="${JUSI_POSTGRES_CONTAINER:-jusi-postgres-dev}"
5
+ POSTGRES_USER="${JUSI_POSTGRES_USER:-jusi}"
6
+ POSTGRES_DB="${JUSI_POSTGRES_DB:-jusi}"
7
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ SQL_PATH="${ROOT_DIR}/docker/postgres/initdb.d/002_jusi_blob_fixture.sql"
9
+
10
+ docker exec -i "${CONTAINER_NAME}" psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" < "${SQL_PATH}"
11
+
12
+ cat <<'EOF'
13
+ Blob fixture installed.
14
+
15
+ Try:
16
+
17
+ %%sql local_postgres
18
+ select id, filename, content_type, payload
19
+ from demo.blob_files;
20
+ EOF
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ IMAGE_NAME="${JUSI_POSTGRES_IMAGE:-jusi-postgres-dev}"
5
+ CONTAINER_NAME="${JUSI_POSTGRES_CONTAINER:-jusi-postgres-dev}"
6
+ HOST_PORT="${JUSI_POSTGRES_PORT:-55432}"
7
+ POSTGRES_USER="${JUSI_POSTGRES_USER:-jusi}"
8
+ POSTGRES_PASSWORD="${JUSI_POSTGRES_PASSWORD:-jusi}"
9
+ POSTGRES_DB="${JUSI_POSTGRES_DB:-jusi}"
10
+
11
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
12
+
13
+ docker build -t "${IMAGE_NAME}" "${ROOT_DIR}"
14
+
15
+ if docker ps --format '{{.Names}}' | grep -Fxq "${CONTAINER_NAME}"; then
16
+ echo "PostgreSQL container '${CONTAINER_NAME}' is already running on port ${HOST_PORT}."
17
+ else
18
+ if docker ps -a --format '{{.Names}}' | grep -Fxq "${CONTAINER_NAME}"; then
19
+ docker rm "${CONTAINER_NAME}" >/dev/null
20
+ fi
21
+
22
+ docker run \
23
+ --detach \
24
+ --name "${CONTAINER_NAME}" \
25
+ --publish "127.0.0.1:${HOST_PORT}:5432" \
26
+ --env "POSTGRES_USER=${POSTGRES_USER}" \
27
+ --env "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \
28
+ --env "POSTGRES_DB=${POSTGRES_DB}" \
29
+ "${IMAGE_NAME}" >/dev/null
30
+ fi
31
+
32
+ echo "Waiting for PostgreSQL to accept connections..."
33
+ for _ in $(seq 1 60); do
34
+ if docker exec "${CONTAINER_NAME}" pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" >/dev/null 2>&1; then
35
+ cat <<EOF
36
+ PostgreSQL is running.
37
+
38
+ jusi.toml:
39
+
40
+ [sql.targets.local_postgres]
41
+ provider = "postgres"
42
+ host = "127.0.0.1"
43
+ port = ${HOST_PORT}
44
+ dbname = "${POSTGRES_DB}"
45
+ user = "${POSTGRES_USER}"
46
+ password = "${POSTGRES_PASSWORD}"
47
+ initial_fetch = 25
48
+ EOF
49
+ exit 0
50
+ fi
51
+ sleep 1
52
+ done
53
+
54
+ echo "PostgreSQL did not become ready in time." >&2
55
+ docker logs "${CONTAINER_NAME}" >&2 || true
56
+ exit 1
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.2.0"
@@ -0,0 +1,19 @@
1
+ """Lightweight Jusi 1.0 catalog provider; imports no runtime dependencies."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from jusi_sql import sql_catalog_entry
7
+
8
+ from . import __version__
9
+
10
+
11
+ def catalog_entry() -> dict[str, Any]:
12
+ return sql_catalog_entry(
13
+ plugin_id="postgres",
14
+ plugin_version=__version__,
15
+ distribution="jusi-postgres",
16
+ kernel_extension="jusi_postgres.kernel",
17
+ worker_entry_point="jusi_postgres.worker:create_worker",
18
+ provider_presentation={"syntax": "pgsql", "indent": "sql"},
19
+ )
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+ from typing import Any, Iterator, Mapping
7
+
8
+
9
+ PLUGIN_OPTIONS = {
10
+ "provider",
11
+ "initial_fetch",
12
+ "krb5ccname",
13
+ "collect_metadata",
14
+ "skip_metadata",
15
+ "metadata_max_rows",
16
+ "metadata_schemas",
17
+ }
18
+
19
+ DEFAULT_METADATA_MAX_ROWS = 1_000_000
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class PostgresOptions:
24
+ connect: dict[str, Any]
25
+ initial_fetch: int
26
+ krb5ccname: str
27
+ collect_metadata: bool
28
+ metadata_max_rows: int
29
+ metadata_schemas: tuple[str, ...]
30
+
31
+
32
+ def parse_postgres_options(options: Mapping[str, Any]) -> PostgresOptions:
33
+ raw_initial = options.get("initial_fetch", 100)
34
+ try:
35
+ initial_fetch = int(raw_initial)
36
+ except (TypeError, ValueError):
37
+ initial_fetch = 100
38
+ if initial_fetch < 0:
39
+ initial_fetch = 0
40
+ krb5ccname = str(options.get("krb5ccname", "")).strip()
41
+ collect_metadata = _parse_bool(options.get("collect_metadata", True), default=True)
42
+ if _parse_bool(options.get("skip_metadata", False), default=False):
43
+ collect_metadata = False
44
+ raw_metadata_max_rows = options.get("metadata_max_rows", DEFAULT_METADATA_MAX_ROWS)
45
+ try:
46
+ metadata_max_rows = int(raw_metadata_max_rows)
47
+ except (TypeError, ValueError):
48
+ metadata_max_rows = DEFAULT_METADATA_MAX_ROWS
49
+ if metadata_max_rows < 1:
50
+ metadata_max_rows = DEFAULT_METADATA_MAX_ROWS
51
+ metadata_schemas = _parse_metadata_schemas(options.get("metadata_schemas", ()))
52
+ connect = {str(key): value for key, value in options.items() if str(key) not in PLUGIN_OPTIONS}
53
+ return PostgresOptions(
54
+ connect=connect,
55
+ initial_fetch=initial_fetch,
56
+ krb5ccname=krb5ccname,
57
+ collect_metadata=collect_metadata,
58
+ metadata_max_rows=metadata_max_rows,
59
+ metadata_schemas=metadata_schemas,
60
+ )
61
+
62
+
63
+ def _parse_bool(value: Any, *, default: bool) -> bool:
64
+ if isinstance(value, bool):
65
+ return value
66
+ if value is None:
67
+ return default
68
+ normalized = str(value).strip().lower()
69
+ if normalized in {"1", "true", "yes", "on"}:
70
+ return True
71
+ if normalized in {"0", "false", "no", "off"}:
72
+ return False
73
+ return default
74
+
75
+
76
+ def _parse_metadata_schemas(value: Any) -> tuple[str, ...]:
77
+ if isinstance(value, str):
78
+ raw_items = value.split(",")
79
+ elif isinstance(value, (list, tuple, set)):
80
+ raw_items = list(value)
81
+ else:
82
+ raw_items = []
83
+ schemas: list[str] = []
84
+ seen: set[str] = set()
85
+ for item in raw_items:
86
+ schema = str(item).strip()
87
+ if not schema or schema in seen:
88
+ continue
89
+ seen.add(schema)
90
+ schemas.append(schema)
91
+ return tuple(schemas)
92
+
93
+
94
+ @contextmanager
95
+ def kerberos_cache_env(path: str) -> Iterator[None]:
96
+ if not path:
97
+ yield
98
+ return
99
+ previous = os.environ.get("KRB5CCNAME")
100
+ os.environ["KRB5CCNAME"] = path
101
+ try:
102
+ yield
103
+ finally:
104
+ if previous is None:
105
+ os.environ.pop("KRB5CCNAME", None)
106
+ else:
107
+ os.environ["KRB5CCNAME"] = previous
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ POSTGRES_BOOTSTRAP_SQL = "SELECT 1 AS jusi_bootstrap WHERE false"