lorebase-store 0.2.3__py3-none-any.whl
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.
- lorebase_store/__init__.py +1 -0
- lorebase_store/_migrations/env.py +106 -0
- lorebase_store/_migrations/script.py.mako +27 -0
- lorebase_store/_migrations/versions/0001_initial_schema.py +1071 -0
- lorebase_store/_migrations/versions/0002_merge_attempt_details.py +74 -0
- lorebase_store/_migrations/versions/0003_webhooks.py +127 -0
- lorebase_store/_migrations/versions/0004_defer_revision_references.py +38 -0
- lorebase_store/_migrations/versions/0005_security_definer_reads.py +396 -0
- lorebase_store/_migrations/versions/0006_api_key_usage.py +48 -0
- lorebase_store/_migrations/versions/0007_document_extraction.py +154 -0
- lorebase_store/_migrations/versions/0008_definer_argument_checks.py +178 -0
- lorebase_store/_migrations/versions/0009_api_key_networks.py +33 -0
- lorebase_store/_migrations/versions/0010_overlay_branches.py +582 -0
- lorebase_store/_migrations/versions/0011_branch_scoped_search_index.py +229 -0
- lorebase_store/_migrations/versions/0012_overlay_neighborhood_lookups.py +185 -0
- lorebase_store/_migrations/versions/0013_web_sessions.py +72 -0
- lorebase_store/_migrations/versions/0014_embedding_jobs.py +70 -0
- lorebase_store/_migrations/versions/0015_append_only_audit_log.py +83 -0
- lorebase_store/_migrations/versions/0016_sealed_webhook_keys.py +55 -0
- lorebase_store/_migrations/versions/0017_background_branch_copies.py +77 -0
- lorebase_store/_migrations/versions/0018_search_lexicon.py +477 -0
- lorebase_store/_migrations/versions/0019_vault_bindings.py +56 -0
- lorebase_store/_migrations/versions/0020_outbox_consumers.py +150 -0
- lorebase_store/_migrations/versions/0021_schema_suggestions.py +114 -0
- lorebase_store/_migrations/versions/0022_read_audit_and_shared_rate_limits.py +133 -0
- lorebase_store/_migrations/versions/0023_agent_trace.py +96 -0
- lorebase_store/_migrations/versions/0024_embedding_template_keys.py +205 -0
- lorebase_store/_migrations/versions/0025_snapshot_refs.py +35 -0
- lorebase_store/_migrations/versions/0026_plain_reverse_link_index.py +112 -0
- lorebase_store/_migrations/versions/0027_vault_rotation_and_tombstones.py +145 -0
- lorebase_store/_migrations/versions/0028_operation_deferred_events.py +71 -0
- lorebase_store/_migrations/versions/0029_agent_trace_sessions.py +108 -0
- lorebase_store/_migrations/versions/0030_path_fanout.py +31 -0
- lorebase_store/_migrations/versions/0031_branch_builds.py +91 -0
- lorebase_store/_migrations/versions/0032_lineage_revisions.py +35 -0
- lorebase_store/_migrations/versions/0033_vault_moved_tokens.py +30 -0
- lorebase_store/_migrations/versions/0034_inference_endpoint_modes.py +73 -0
- lorebase_store/_migrations/versions/0035_search_truncation.py +104 -0
- lorebase_store/_migrations/versions/0036_lineage_maps.py +325 -0
- lorebase_store/_migrations/versions/0037_bm25_search.py +346 -0
- lorebase_store/auth/__init__.py +52 -0
- lorebase_store/auth/api_keys.py +161 -0
- lorebase_store/auth/audit.py +382 -0
- lorebase_store/auth/checkpoints.py +185 -0
- lorebase_store/auth/grants.py +161 -0
- lorebase_store/auth/principals.py +158 -0
- lorebase_store/auth/scope.py +70 -0
- lorebase_store/auth/sessions.py +153 -0
- lorebase_store/auth/unit.py +53 -0
- lorebase_store/auth/usage.py +70 -0
- lorebase_store/blobs.py +73 -0
- lorebase_store/bm25.py +346 -0
- lorebase_store/bulk.py +147 -0
- lorebase_store/bundle.py +1830 -0
- lorebase_store/changefeed.py +261 -0
- lorebase_store/commit/__init__.py +42 -0
- lorebase_store/commit/changes.py +67 -0
- lorebase_store/commit/errors.py +60 -0
- lorebase_store/commit/migrations.py +335 -0
- lorebase_store/commit/pipeline.py +754 -0
- lorebase_store/commit/schema.py +123 -0
- lorebase_store/db.py +58 -0
- lorebase_store/definer.py +50 -0
- lorebase_store/digests.py +99 -0
- lorebase_store/embeddings.py +444 -0
- lorebase_store/endpoint_modes.py +59 -0
- lorebase_store/extraction/__init__.py +61 -0
- lorebase_store/extraction/items.py +187 -0
- lorebase_store/extraction/materialize.py +364 -0
- lorebase_store/extraction/proposal.py +65 -0
- lorebase_store/extraction/review.py +168 -0
- lorebase_store/gc.py +970 -0
- lorebase_store/indexes.py +1033 -0
- lorebase_store/jobs.py +651 -0
- lorebase_store/keyring.py +72 -0
- lorebase_store/layout.py +335 -0
- lorebase_store/materialize/__init__.py +103 -0
- lorebase_store/materialize/apply.py +436 -0
- lorebase_store/materialize/background.py +1184 -0
- lorebase_store/materialize/branch.py +268 -0
- lorebase_store/materialize/build.py +536 -0
- lorebase_store/materialize/derived.py +540 -0
- lorebase_store/materialize/lineage_backfill.py +502 -0
- lorebase_store/materialize/links.py +266 -0
- lorebase_store/materialize/overlay.py +292 -0
- lorebase_store/materialize/paths.py +315 -0
- lorebase_store/materialize/rebuild.py +326 -0
- lorebase_store/materialize/rows.py +153 -0
- lorebase_store/materialize/sensitivity.py +66 -0
- lorebase_store/materialize/snapshot.py +205 -0
- lorebase_store/materialize/stale.py +444 -0
- lorebase_store/materialize/verify.py +321 -0
- lorebase_store/merging/__init__.py +39 -0
- lorebase_store/merging/attempts.py +334 -0
- lorebase_store/merging/lineage.py +123 -0
- lorebase_store/merging/service.py +652 -0
- lorebase_store/migrations.py +133 -0
- lorebase_store/models/__init__.py +136 -0
- lorebase_store/models/base.py +64 -0
- lorebase_store/models/enums.py +132 -0
- lorebase_store/models/governance.py +268 -0
- lorebase_store/models/ingestion.py +288 -0
- lorebase_store/models/materialized.py +277 -0
- lorebase_store/models/proposals.py +195 -0
- lorebase_store/models/queues.py +87 -0
- lorebase_store/models/tracing.py +116 -0
- lorebase_store/models/versioning.py +222 -0
- lorebase_store/models/webhooks.py +96 -0
- lorebase_store/nodestore.py +384 -0
- lorebase_store/objectstore.py +162 -0
- lorebase_store/outbox_events.py +166 -0
- lorebase_store/profiling/__init__.py +58 -0
- lorebase_store/profiling/delimited.py +109 -0
- lorebase_store/profiling/postgres.py +211 -0
- lorebase_store/profiling/record_files.py +127 -0
- lorebase_store/profiling/records.py +92 -0
- lorebase_store/profiling/sql_databases.py +298 -0
- lorebase_store/ratelimit.py +129 -0
- lorebase_store/reads.py +1491 -0
- lorebase_store/replicas.py +434 -0
- lorebase_store/repositories/__init__.py +152 -0
- lorebase_store/repositories/changesets.py +59 -0
- lorebase_store/repositories/deferred_events.py +142 -0
- lorebase_store/repositories/documents.py +305 -0
- lorebase_store/repositories/domains.py +97 -0
- lorebase_store/repositories/events.py +205 -0
- lorebase_store/repositories/index_view.py +489 -0
- lorebase_store/repositories/invocations.py +68 -0
- lorebase_store/repositories/materializations.py +276 -0
- lorebase_store/repositories/materialized.py +1129 -0
- lorebase_store/repositories/merges.py +417 -0
- lorebase_store/repositories/proposal_items.py +199 -0
- lorebase_store/repositories/proposals.py +184 -0
- lorebase_store/repositories/provenance.py +77 -0
- lorebase_store/repositories/refs.py +452 -0
- lorebase_store/repositories/revisions.py +273 -0
- lorebase_store/repositories/scope.py +33 -0
- lorebase_store/repositories/source_runs.py +123 -0
- lorebase_store/repositories/suggestions.py +638 -0
- lorebase_store/runtime.py +87 -0
- lorebase_store/schema_cache.py +50 -0
- lorebase_store/settings.py +879 -0
- lorebase_store/storage.py +1478 -0
- lorebase_store/telemetry.py +142 -0
- lorebase_store/testing.py +108 -0
- lorebase_store/tracing.py +505 -0
- lorebase_store/traverse.py +578 -0
- lorebase_store/vault/__init__.py +50 -0
- lorebase_store/vault/keys.py +216 -0
- lorebase_store/vault/quotes.py +133 -0
- lorebase_store/vault/rotation.py +209 -0
- lorebase_store/vault/settings.py +152 -0
- lorebase_store/vault/store.py +987 -0
- lorebase_store/vault/transfer.py +558 -0
- lorebase_store/webhooks/__init__.py +24 -0
- lorebase_store/webhooks/delivery.py +637 -0
- lorebase_store/webhooks/keys.py +126 -0
- lorebase_store/webhooks/repository.py +142 -0
- lorebase_store/webhooks/settings.py +138 -0
- lorebase_store/webhooks/signing.py +50 -0
- lorebase_store-0.2.3.dist-info/METADATA +13 -0
- lorebase_store-0.2.3.dist-info/RECORD +163 -0
- lorebase_store-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""OpenTelemetry instruments of the store. They are no-ops until the process installs
|
|
2
|
+
providers (`lorebase_platform.telemetry.setup_telemetry`, `telemetry.otlp_endpoint` set); the
|
|
3
|
+
process running the store code (Keeper or Scribe) exports them under its own service name."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from threading import Lock
|
|
9
|
+
|
|
10
|
+
from opentelemetry import metrics
|
|
11
|
+
from opentelemetry.metrics import CallbackOptions, Observation
|
|
12
|
+
|
|
13
|
+
from lorebase_platform.telemetry import LATENCY_BUCKETS_S
|
|
14
|
+
|
|
15
|
+
INSTRUMENTATION = "lorebase.store"
|
|
16
|
+
|
|
17
|
+
meter = metrics.get_meter(INSTRUMENTATION)
|
|
18
|
+
|
|
19
|
+
COMMIT_DURATION = meter.create_histogram(
|
|
20
|
+
"lorebase.store.commit.duration",
|
|
21
|
+
unit="s",
|
|
22
|
+
description="Duration of a successful commit through the commit pipeline, by origin.",
|
|
23
|
+
explicit_bucket_boundaries_advisory=LATENCY_BUCKETS_S,
|
|
24
|
+
)
|
|
25
|
+
MERGE_ATTEMPTS = meter.create_counter(
|
|
26
|
+
"lorebase.store.merge.attempts",
|
|
27
|
+
unit="{attempt}",
|
|
28
|
+
description="Merges of a Proposal, by result (merged, fast_forward, up_to_date, conflicts).",
|
|
29
|
+
)
|
|
30
|
+
DEFINER_READS = meter.create_counter(
|
|
31
|
+
"lorebase.store.reads.definer",
|
|
32
|
+
unit="{call}",
|
|
33
|
+
description="Calls of the security definer read functions, by function.",
|
|
34
|
+
)
|
|
35
|
+
JOBS_CLAIMED = meter.create_counter(
|
|
36
|
+
"lorebase.jobs.claimed", unit="{job}", description="Jobs claimed from the queue, by type."
|
|
37
|
+
)
|
|
38
|
+
JOBS_RECLAIMED = meter.create_counter(
|
|
39
|
+
"lorebase.jobs.reclaimed",
|
|
40
|
+
unit="{job}",
|
|
41
|
+
description="Running jobs whose worker stopped sending heartbeats, taken back from it, by "
|
|
42
|
+
"type and outcome (requeued, failed).",
|
|
43
|
+
)
|
|
44
|
+
JOBS_RELEASED = meter.create_counter(
|
|
45
|
+
"lorebase.jobs.released",
|
|
46
|
+
unit="{job}",
|
|
47
|
+
description="Running jobs a stopping worker put back in the queue, by type.",
|
|
48
|
+
)
|
|
49
|
+
WEBHOOK_OUTCOMES = meter.create_counter(
|
|
50
|
+
"lorebase.webhooks.deliveries",
|
|
51
|
+
unit="{delivery}",
|
|
52
|
+
description="Webhook delivery attempts, by outcome (delivered, retrying, dead).",
|
|
53
|
+
)
|
|
54
|
+
WEBHOOK_TIMEOUTS = meter.create_counter(
|
|
55
|
+
"lorebase.webhooks.timeouts",
|
|
56
|
+
unit="{attempt}",
|
|
57
|
+
description="Webhook delivery attempts cut off by webhooks.timeout_s.",
|
|
58
|
+
)
|
|
59
|
+
OUTBOX_HANDOFFS = meter.create_counter(
|
|
60
|
+
"lorebase.outbox.handoffs",
|
|
61
|
+
unit="{event}",
|
|
62
|
+
description="Outbox events handed off by a consumer, by consumer.",
|
|
63
|
+
)
|
|
64
|
+
OUTBOX_PRUNED = meter.create_counter(
|
|
65
|
+
"lorebase.outbox.pruned",
|
|
66
|
+
unit="{event}",
|
|
67
|
+
description="Outbox events deleted, by reason (retention: handed off by every consumer; "
|
|
68
|
+
"expired: older than store.outbox.max_age_s while still pending).",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PruneBacklog:
|
|
73
|
+
"""Outbox events still eligible for pruning after the latest pruning pass of this process
|
|
74
|
+
(capped, see `OutboxRepository.prunable`); nothing is reported before the first pass."""
|
|
75
|
+
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
self._lock = Lock()
|
|
78
|
+
self._value: int | None = None
|
|
79
|
+
|
|
80
|
+
def set(self, value: int) -> None:
|
|
81
|
+
with self._lock:
|
|
82
|
+
self._value = value
|
|
83
|
+
|
|
84
|
+
def observe(self, _options: CallbackOptions | None = None) -> Iterable[Observation]:
|
|
85
|
+
with self._lock:
|
|
86
|
+
value = self._value
|
|
87
|
+
return [] if value is None else [Observation(value)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
OUTBOX_PRUNE_BACKLOG = PruneBacklog()
|
|
91
|
+
meter.create_observable_gauge(
|
|
92
|
+
"lorebase.outbox.prune.backlog",
|
|
93
|
+
callbacks=[OUTBOX_PRUNE_BACKLOG.observe],
|
|
94
|
+
unit="{event}",
|
|
95
|
+
description="Outbox events eligible for pruning left after the latest pruning pass "
|
|
96
|
+
"(counted up to a cap).",
|
|
97
|
+
)
|
|
98
|
+
OPERATION_EVENTS = meter.create_counter(
|
|
99
|
+
"lorebase.operations.events",
|
|
100
|
+
unit="{event}",
|
|
101
|
+
description="Operation side effect events written to the outbox with a committed execution.",
|
|
102
|
+
)
|
|
103
|
+
GC_RUNS = meter.create_counter(
|
|
104
|
+
"lorebase.gc.runs", unit="{run}", description="Completed garbage collections of a Domain."
|
|
105
|
+
)
|
|
106
|
+
GC_FREED_BYTES = meter.create_counter(
|
|
107
|
+
"lorebase.gc.freed_bytes",
|
|
108
|
+
unit="By",
|
|
109
|
+
description="Bytes of tree nodes and objects deleted by garbage collection.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
BRANCH_JOB_ROWS = meter.create_counter(
|
|
113
|
+
"lorebase.store.branch_jobs.rows",
|
|
114
|
+
unit="{row}",
|
|
115
|
+
description="Rows read by background branch jobs, by job (materialize: base rows copied "
|
|
116
|
+
"to a branch, delete: rows of a deleted branch removed, sensitivity_refresh: Records whose "
|
|
117
|
+
"title and search vector were checked).",
|
|
118
|
+
)
|
|
119
|
+
BRANCH_JOB_RUNS = meter.create_counter(
|
|
120
|
+
"lorebase.store.branch_jobs.runs",
|
|
121
|
+
unit="{run}",
|
|
122
|
+
description="Runs of background branch jobs, by job and outcome (done, deferred, waiting, "
|
|
123
|
+
"skipped, cancelled, failed; for copies also retried: failed for good, queued again "
|
|
124
|
+
"with backoff; fallback: the overlay was detached and is rebuilt from its tree; "
|
|
125
|
+
"given_up: no attempt left).",
|
|
126
|
+
)
|
|
127
|
+
BRANCH_JOB_BATCH_ROWS = meter.create_histogram(
|
|
128
|
+
"lorebase.store.branch_jobs.batch.rows",
|
|
129
|
+
unit="{row}",
|
|
130
|
+
description="Rows a batch of a background branch job was sized for, by job: batches "
|
|
131
|
+
"shrink while commits wait for the branch lock and grow back when none does.",
|
|
132
|
+
)
|
|
133
|
+
BRANCH_JOB_BATCH_DURATION = meter.create_histogram(
|
|
134
|
+
"lorebase.store.branch_jobs.batch.duration",
|
|
135
|
+
unit="s",
|
|
136
|
+
description="Duration of one batch transaction of a background branch job, by job.",
|
|
137
|
+
explicit_bucket_boundaries_advisory=LATENCY_BUCKETS_S,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def record_definer_call(function: str) -> None:
|
|
142
|
+
DEFINER_READS.add(1, {"function": function})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Postgres for tests: an existing server or a disposable container.
|
|
2
|
+
|
|
3
|
+
`postgres_server()` yields an asyncpg URL of a server where the role may create databases and
|
|
4
|
+
roles. It uses `LOREBASE_TEST_DATABASE_URL` when set; otherwise it starts a Postgres 16 +
|
|
5
|
+
pgvector container with testcontainers (image overridable with `LOREBASE_TEST_POSTGRES_IMAGE`).
|
|
6
|
+
`scratch_database()` creates an empty database on that server and drops it afterwards.
|
|
7
|
+
`nested_plans()` collects the plans of statements run inside functions (`auto_explain`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import AsyncIterator, Iterator
|
|
17
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from sqlalchemy import URL, text
|
|
21
|
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
22
|
+
from sqlalchemy.pool import NullPool
|
|
23
|
+
|
|
24
|
+
from lorebase_store.db import async_url
|
|
25
|
+
|
|
26
|
+
DATABASE_URL_ENV = "LOREBASE_TEST_DATABASE_URL"
|
|
27
|
+
IMAGE_ENV = "LOREBASE_TEST_POSTGRES_IMAGE"
|
|
28
|
+
DEFAULT_IMAGE = "pgvector/pgvector:pg16"
|
|
29
|
+
SCRATCH_PREFIX = "lorebase_test_"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def postgres_server() -> Iterator[str]:
|
|
34
|
+
"""URL of a Postgres server for integration tests."""
|
|
35
|
+
configured = os.environ.get(DATABASE_URL_ENV)
|
|
36
|
+
if configured:
|
|
37
|
+
yield async_url(configured).render_as_string(hide_password=False)
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
from testcontainers.community.postgres import PostgresContainer
|
|
41
|
+
except ImportError as exc:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
f"set {DATABASE_URL_ENV} or install the store dev dependencies "
|
|
44
|
+
"(uv sync --all-packages) to start a Postgres container"
|
|
45
|
+
) from exc
|
|
46
|
+
with PostgresContainer(os.environ.get(IMAGE_ENV, DEFAULT_IMAGE), driver="asyncpg") as pg:
|
|
47
|
+
yield pg.get_connection_url()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _execute_autocommit(url: URL, statement: str) -> None:
|
|
51
|
+
engine = create_async_engine(url, poolclass=NullPool, isolation_level="AUTOCOMMIT")
|
|
52
|
+
try:
|
|
53
|
+
async with engine.connect() as connection:
|
|
54
|
+
await connection.execute(text(statement))
|
|
55
|
+
finally:
|
|
56
|
+
await engine.dispose()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_AUTO_EXPLAIN = (
|
|
60
|
+
"LOAD 'auto_explain'",
|
|
61
|
+
"SET LOCAL auto_explain.log_min_duration = 0",
|
|
62
|
+
"SET LOCAL auto_explain.log_nested_statements = on",
|
|
63
|
+
"SET LOCAL auto_explain.log_format = json",
|
|
64
|
+
"SET LOCAL client_min_messages = log",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@asynccontextmanager
|
|
69
|
+
async def nested_plans(session: AsyncSession) -> AsyncIterator[list[dict[str, Any]]]:
|
|
70
|
+
"""Plans (EXPLAIN JSON `Plan` nodes) of the statements the session's transaction runs
|
|
71
|
+
meanwhile, including those run inside functions, as `auto_explain` reports them.
|
|
72
|
+
|
|
73
|
+
Loading the module needs a superuser: enter before `SET LOCAL ROLE`. Raises
|
|
74
|
+
`sqlalchemy.exc.DBAPIError` where the module cannot be loaded.
|
|
75
|
+
"""
|
|
76
|
+
connection = await session.connection()
|
|
77
|
+
for statement in _AUTO_EXPLAIN:
|
|
78
|
+
await connection.exec_driver_sql(statement)
|
|
79
|
+
driver = (await connection.get_raw_connection()).driver_connection
|
|
80
|
+
assert driver is not None
|
|
81
|
+
plans: list[dict[str, Any]] = []
|
|
82
|
+
|
|
83
|
+
def listen(_connection: Any, message: Any) -> None:
|
|
84
|
+
_, marker, body = str(message.message).partition("plan:\n")
|
|
85
|
+
if marker:
|
|
86
|
+
plans.append(json.loads(body)["Plan"])
|
|
87
|
+
|
|
88
|
+
driver.add_log_listener(listen)
|
|
89
|
+
try:
|
|
90
|
+
yield plans
|
|
91
|
+
finally:
|
|
92
|
+
# A top-level statement's plan is logged when its portal closes, at the next statement.
|
|
93
|
+
await connection.exec_driver_sql("SELECT 1")
|
|
94
|
+
await asyncio.sleep(0) # log messages are dispatched through the event loop
|
|
95
|
+
driver.remove_log_listener(listen)
|
|
96
|
+
await connection.exec_driver_sql("SET LOCAL client_min_messages = notice")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def scratch_database(server_url: str) -> Iterator[str]:
|
|
101
|
+
"""Create a uniquely named empty database on `server_url`, yield its URL, then drop it."""
|
|
102
|
+
server = async_url(server_url)
|
|
103
|
+
name = f"{SCRATCH_PREFIX}{uuid.uuid4().hex[:12]}"
|
|
104
|
+
asyncio.run(_execute_autocommit(server, f'CREATE DATABASE "{name}"'))
|
|
105
|
+
try:
|
|
106
|
+
yield server.set(database=name).render_as_string(hide_password=False)
|
|
107
|
+
finally:
|
|
108
|
+
asyncio.run(_execute_autocommit(server, f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)'))
|