Extend playground services¶
The public playground service layer supports custom launchers, configuration tooling, schema refresh integrations, and migration workflow tests. Textual screen and widget classes remain implementation details.
Configuration records and functions¶
Use these records to discover, parse, select, validate, and write playground configuration.
ormdantic.playground.config
¶
Project configuration for the Ormdantic playground.
ConfigError
¶
Bases: ValueError
Raised when playground configuration is invalid or incomplete.
ProjectConfig
dataclass
¶
ProjectConfig(target=None, migrations_dir=Path('migrations'), format='toml', watch=DEFAULT_WATCH, database_poll_seconds=5.0, debounce_milliseconds=300)
Project-wide playground settings.
EnvironmentConfig
dataclass
¶
EnvironmentConfig(name, url_env='DATABASE_URL', env_file=Path('.env'), safety='confirm', production=False)
Connection lookup and safety settings for a named environment.
PlaygroundConfig
dataclass
¶
PlaygroundConfig(project, environments)
EffectiveConfig
dataclass
¶
EffectiveConfig(path, root, project, environment)
DatabaseUrlSource
dataclass
¶
DatabaseUrlSource(value, label)
discover_config
¶
discover_config(start)
Find the nearest playground configuration at or above start.
Source code in ormdantic/playground/config.py
def discover_config(start: Path) -> Path | None:
"""Find the nearest playground configuration at or above ``start``."""
current = start.resolve()
if current.is_file():
current = current.parent
for directory in (current, *current.parents):
candidate = directory / CONFIG_NAME
if candidate.is_file():
return candidate
return None
parse_config
¶
parse_config(path)
Parse a configuration while retaining its relative paths.
Source code in ormdantic/playground/config.py
def parse_config(path: Path) -> PlaygroundConfig:
"""Parse a configuration while retaining its relative paths."""
try:
source = path.read_text(encoding="utf-8")
except FileNotFoundError:
raise
return parse_config_source(source, source_name=str(path))
parse_config_source
¶
parse_config_source(source, *, source_name='ormdantic.toml')
Parse and validate an in-memory playground TOML document.
Source code in ormdantic/playground/config.py
def parse_config_source(
source: str,
*,
source_name: str = "ormdantic.toml",
) -> PlaygroundConfig:
"""Parse and validate an in-memory playground TOML document."""
try:
payload = toml_loads(source)
except Exception as exc:
raise ConfigError(f"{source_name}: invalid TOML: {exc}") from exc
_reject_unknown(payload, {"project", "environments"}, "")
project_payload = _table(payload.get("project"), "project")
environments_payload = _table(payload.get("environments"), "environments")
if not environments_payload:
raise ConfigError("environments: configure at least one named environment")
project = _parse_project(project_payload)
environments = {
str(name): _parse_environment(
str(name),
_table(value, f"environments.{name}"),
)
for name, value in environments_payload.items()
}
return PlaygroundConfig(project=project, environments=environments)
load_config
¶
load_config(path, *, environment=None, target=None, migrations_dir=None)
Load, validate, select, and resolve a playground configuration.
Source code in ormdantic/playground/config.py
def load_config(
path: Path,
*,
environment: str | None = None,
target: str | None = None,
migrations_dir: Path | None = None,
) -> EffectiveConfig:
"""Load, validate, select, and resolve a playground configuration."""
config_path = path.resolve()
config = parse_config(config_path)
environment_name = environment or "development"
selected = config.environments.get(environment_name)
if selected is None:
raise ConfigError(
f"environments.{environment_name}: named environment is not configured"
)
root = config_path.parent
project = config.project
effective_target = _optional_nonempty_string(
target if target is not None else project.target,
"project.target",
)
if effective_target is None:
raise ConfigError("project.target: import target is required")
migration_path = migrations_dir or project.migrations_dir
resolved_project = replace(
project,
target=effective_target,
migrations_dir=_resolve_path(root, migration_path),
)
resolved_environment = replace(
selected,
env_file=(
_resolve_path(root, selected.env_file)
if selected.env_file is not None
else None
),
safety="typed" if selected.production else selected.safety,
)
return EffectiveConfig(
path=config_path,
root=root,
project=resolved_project,
environment=resolved_environment,
)
write_config
¶
write_config(path, config, *, overwrite=False)
Atomically write a canonical playground TOML file.
Source code in ormdantic/playground/config.py
def write_config(
path: Path,
config: PlaygroundConfig,
*,
overwrite: bool = False,
) -> None:
"""Atomically write a canonical playground TOML file."""
destination = path.resolve()
if destination.exists() and not overwrite:
raise FileExistsError(destination)
source = _config_toml(config)
_atomic_write(destination, source)
write_config_source
¶
write_config_source(path, source)
Validate then atomically replace an existing playground TOML file.
Source code in ormdantic/playground/config.py
def write_config_source(path: Path, source: str) -> None:
"""Validate then atomically replace an existing playground TOML file."""
destination = path.resolve()
parse_config_source(source, source_name=str(destination))
_atomic_write(destination, source)
resolve_database_url
¶
resolve_database_url(environment)
Resolve a URL from the process environment and then its dotenv file.
Source code in ormdantic/playground/config.py
def resolve_database_url(environment: EnvironmentConfig) -> DatabaseUrlSource:
"""Resolve a URL from the process environment and then its dotenv file."""
env_value = _clean_value(os.environ.get(environment.url_env))
if env_value is not None:
return DatabaseUrlSource(env_value, environment.url_env)
dotenv_values = _read_env_file(environment.env_file)
dotenv_value = _clean_value(dotenv_values.get(environment.url_env))
if dotenv_value is not None:
return DatabaseUrlSource(
dotenv_value,
f"{environment.env_file}:{environment.url_env}",
)
raise ConfigError(
f"environments.{environment.name}.url_env: export {environment.url_env} "
f"or define it in {environment.env_file or '.env'}"
)
Immutable state records¶
State snapshots keep UI rendering and background services independent.
ormdantic.playground.state
¶
Immutable state snapshots shared by playground services and views.
RefreshStatus
¶
Bases: str, Enum
Lifecycle state for schema refreshes.
SchemaState
dataclass
¶
SchemaState(model_snapshot=None, live_snapshot=None, diff=None, forward_sql=(), rollback_sql=(), refreshed_at=None, stale=False, diagnostics=())
The most recent model/live snapshots and derived drift.
ready_for_generation
property
¶
ready_for_generation
Whether a fresh, complete, non-empty migration plan is available.
ArtifactSummary
dataclass
¶
ArtifactSummary(path, revision, status, format, destructive=False, unsafe=False, valid=True)
Render-safe metadata for one migration artifact.
MigrationState
dataclass
¶
MigrationState(artifacts=(), history=(), current_revision=None, dirty=False, selected_path=None)
Migration files and durable history known to the playground.
OperationState
dataclass
¶
OperationState(name=None, target=None, running=False, message=None)
PlaygroundState
dataclass
¶
PlaygroundState(environment, connection_label=None, dialect=None, generation=0, status=IDLE, watcher_paused=False, schema=SchemaState(), migrations=MigrationState(), operation=OperationState(), diagnostics=())
Complete immutable application state.
RefreshResult
dataclass
¶
RefreshResult(generation, status, schema, migrations=None, connection_label=None, dialect=None, diagnostics=())
One generation of refresh work ready for publication.
accept_refresh
¶
accept_refresh(state, result)
Publish a refresh unless a newer generation already won the race.
Source code in ormdantic/playground/state.py
def accept_refresh(
state: PlaygroundState,
result: RefreshResult,
) -> PlaygroundState:
"""Publish a refresh unless a newer generation already won the race."""
if result.generation < state.generation:
return state
changes: dict[str, object] = {
"generation": result.generation,
"status": result.status,
"schema": result.schema,
"connection_label": result.connection_label,
"dialect": result.dialect,
"diagnostics": result.diagnostics,
}
if result.migrations is not None:
changes["migrations"] = result.migrations
return replace(state, **changes)
Diagnostics and redaction¶
Create diagnostics through Diagnostic.create so messages and structured details are redacted before publication.
ormdantic.playground.diagnostics
¶
Structured, secret-safe diagnostics for the playground.
Severity
¶
Diagnostic
dataclass
¶
Diagnostic(severity, code, message, source=None, hint=None, details=dict())
One actionable and already-redacted diagnostic.
create
classmethod
¶
create(severity, code, message, *, source=None, hint=None, details=None)
Construct a diagnostic after removing secret values.
Source code in ormdantic/playground/diagnostics.py
@classmethod
def create(
cls,
severity: Severity,
code: str,
message: str,
*,
source: str | None = None,
hint: str | None = None,
details: Mapping[str, Any] | None = None,
) -> Diagnostic:
"""Construct a diagnostic after removing secret values."""
return cls(
severity=severity,
code=code,
message=redact_text(message),
source=redact_text(source) if source is not None else None,
hint=redact_text(hint) if hint is not None else None,
details=redact_value(details or {}),
)
redact_text
¶
redact_text(value)
Redact credentials embedded in URLs or secret query parameters.
Source code in ormdantic/playground/diagnostics.py
def redact_text(value: str) -> str:
"""Redact credentials embedded in URLs or secret query parameters."""
value = _CREDENTIAL_URL.sub(
lambda match: f"{match.group('scheme')}{match.group('user')}:<redacted>@",
value,
)
return _SECRET_PARAMETER.sub(
lambda match: f"{match.group('prefix')}<redacted>",
value,
)
redact_value
¶
redact_value(value, *, key=None)
Recursively redact values carried by diagnostics and logs.
Source code in ormdantic/playground/diagnostics.py
def redact_value(value: Any, *, key: str | None = None) -> Any:
"""Recursively redact values carried by diagnostics and logs."""
if key is not None and key.casefold() in _SECRET_KEYS:
return "<redacted>"
if isinstance(value, str):
return redact_text(value)
if isinstance(value, Mapping):
return {
str(item_key): redact_value(item, key=str(item_key))
for item_key, item in value.items()
}
if isinstance(value, Sequence) and not isinstance(value, bytes | bytearray | str):
return tuple(redact_value(item) for item in value)
return value
Inspection and refresh services¶
inspect_models isolates imports in a child process. RefreshService coordinates model inspection, live reflection, planning, and history without blocking the TUI event loop.
ormdantic.playground.inspection
¶
Async client for isolated model inspection.
InspectionError
dataclass
¶
InspectionError(type, message)
InspectionResult
dataclass
¶
InspectionResult(snapshot, diagnostics=(), error=None)
inspect_models
async
¶
inspect_models(target, *, cwd, timeout=15.0, process_factory=None)
Inspect models in a subprocess without polluting the TUI process.
Source code in ormdantic/playground/inspection.py
async def inspect_models(
target: str,
*,
cwd: Path,
timeout: float = 15.0,
process_factory: ProcessFactory | None = None,
) -> InspectionResult:
"""Inspect models in a subprocess without polluting the TUI process."""
spawn = process_factory or cast(ProcessFactory, asyncio.create_subprocess_exec)
process = await spawn(
sys.executable,
"-m",
"ormdantic.playground.inspect_worker",
target,
cwd=str(cwd),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout)
except asyncio.CancelledError:
await asyncio.shield(_terminate_process(process))
raise
except asyncio.TimeoutError:
await _terminate_process(process)
message = f"Model inspection exceeded {timeout:g} seconds"
return InspectionResult(
snapshot=None,
error=InspectionError("TimeoutError", message),
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"model.timeout",
message,
hint="Fix slow import-time work or increase the inspection timeout.",
),
),
)
stdout_text = stdout.decode("utf-8", errors="replace").strip()
stderr_text = stderr.decode("utf-8", errors="replace").strip()
if process.returncode not in {0, None}:
message = stderr_text or f"Inspection worker exited with {process.returncode}"
return InspectionResult(
snapshot=None,
error=InspectionError("WorkerError", message),
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"model.worker_failed",
message,
details={"returncode": process.returncode},
),
),
)
try:
payload = json.loads(stdout_text)
if not isinstance(payload, Mapping):
raise TypeError("worker payload must be a JSON object")
if payload.get("protocol") != PROTOCOL_VERSION:
raise ValueError(f"unsupported worker protocol {payload.get('protocol')!r}")
diagnostics = _diagnostics(payload.get("diagnostics", []))
if payload.get("ok") is True:
snapshot_payload = payload.get("snapshot")
if not isinstance(snapshot_payload, Mapping):
raise TypeError("successful worker payload has no snapshot")
return InspectionResult(
snapshot=SchemaSnapshot.from_dict(snapshot_payload),
diagnostics=diagnostics,
)
error = _inspection_error(payload.get("error"))
return InspectionResult(
snapshot=None,
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"model.import_failed",
f"{error.type}: {error.message}",
hint="Fix the target or its import error, then refresh.",
),
*diagnostics,
),
error=error,
)
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
message = f"Invalid model-inspection response: {exc}"
if stderr_text:
message = f"{message}. Worker stderr: {stderr_text}"
return InspectionResult(
snapshot=None,
error=InspectionError(type(exc).__name__, str(exc)),
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"model.protocol_error",
message,
hint="Reinstall matching Ormdantic package versions.",
),
),
)
ormdantic.playground.services
¶
Framework-neutral schema refresh services.
DatabaseRefresh
dataclass
¶
DatabaseRefresh(live_snapshot, history=(), current_revision=None, dirty=False, diagnostics=())
Live database data collected by a blocking worker.
RefreshService
¶
RefreshService(*, inspector=inspect_models, url_resolver=resolve_database_url, reflector=None, planner=None, thread_runner=to_thread)
Coordinate isolated model imports with threaded database reflection.
Source code in ormdantic/playground/services.py
def __init__(
self,
*,
inspector: Callable[..., Any] = inspect_models,
url_resolver: Callable[..., DatabaseUrlSource] = resolve_database_url,
reflector: Callable[[str], DatabaseRefresh] | None = None,
planner: Callable[[str, SchemaSnapshot, SchemaSnapshot], MigrationPlan]
| None = None,
thread_runner: Callable[..., Any] = asyncio.to_thread,
) -> None:
self._inspector = inspector
self._url_resolver = url_resolver
self._reflector = reflector or reflect_database
self._planner = planner or plan_migration
self._thread_runner = thread_runner
refresh
async
¶
refresh(config, *, generation, previous=None)
Build one refresh generation without blocking the UI loop.
Source code in ormdantic/playground/services.py
async def refresh(
self,
config: EffectiveConfig,
*,
generation: int,
previous: SchemaState | None = None,
) -> RefreshResult:
"""Build one refresh generation without blocking the UI loop."""
target = config.project.target
if target is None: # guarded by config loading; retained for typed callers
raise ValueError("project target is required")
model_task = asyncio.create_task(
self._inspector(target, cwd=config.root),
name=f"ormdantic-model-refresh-{generation}",
)
diagnostics: list[Diagnostic] = []
resolved: DatabaseUrlSource | None = None
database_task: asyncio.Task[DatabaseRefresh] | None = None
try:
resolved = self._url_resolver(config.environment)
except Exception as exc:
diagnostics.append(
Diagnostic.create(
Severity.ERROR,
"database.url_missing",
str(exc),
hint="Configure the environment variable or dotenv file.",
)
)
else:
database_task = asyncio.create_task(
self._thread_runner(self._reflector, resolved.value),
name=f"ormdantic-database-refresh-{generation}",
)
model_result: InspectionResult | None = None
database_result: DatabaseRefresh | None = None
model_value = await _task_result(model_task)
if isinstance(model_value, Exception):
diagnostics.append(
Diagnostic.create(
Severity.ERROR,
"model.inspection_failed",
str(model_value),
hint="Fix the model target and refresh.",
)
)
else:
model_result = model_value
diagnostics.extend(model_result.diagnostics)
if database_task is not None:
database_value = await _task_result(database_task)
if isinstance(database_value, Exception):
diagnostics.append(
Diagnostic.create(
Severity.ERROR,
"database.reflection_failed",
str(database_value),
hint="Check connectivity and database permissions.",
)
)
else:
database_result = database_value
diagnostics.extend(database_result.diagnostics)
fresh_model_snapshot = (
model_result.snapshot if model_result is not None else None
)
fresh_live_snapshot = (
database_result.live_snapshot if database_result is not None else None
)
plan: MigrationPlan | None = None
if (
fresh_model_snapshot is not None
and fresh_live_snapshot is not None
and resolved is not None
):
try:
plan = await self._thread_runner(
self._planner,
resolved.value,
fresh_live_snapshot,
fresh_model_snapshot,
)
except Exception as exc:
diagnostics.append(
Diagnostic.create(
Severity.ERROR,
"schema.plan_failed",
str(exc),
hint="Review dialect support for the reported schema change.",
)
)
available = sum(
item is not None for item in (fresh_model_snapshot, fresh_live_snapshot)
)
has_errors = any(item.severity is Severity.ERROR for item in diagnostics)
if available == 2 and not has_errors:
status = RefreshStatus.HEALTHY
elif available:
status = RefreshStatus.PARTIAL
else:
status = RefreshStatus.ERROR
model_snapshot = fresh_model_snapshot or (
previous.model_snapshot if previous is not None else None
)
live_snapshot = fresh_live_snapshot or (
previous.live_snapshot if previous is not None else None
)
schema = SchemaState(
model_snapshot=model_snapshot,
live_snapshot=live_snapshot,
diff=plan.diff
if plan is not None
else (previous.diff if previous else None),
forward_sql=(
tuple(operation.sql for operation in plan.operations)
if plan
else (previous.forward_sql if previous else ())
),
rollback_sql=(
tuple(operation.sql for operation in plan.rollback_operations)
if plan
else (previous.rollback_sql if previous else ())
),
refreshed_at=datetime.now(UTC),
stale=status is not RefreshStatus.HEALTHY,
diagnostics=tuple(diagnostics),
)
migrations = MigrationState(
history=database_result.history if database_result else (),
current_revision=(
database_result.current_revision if database_result else None
),
dirty=database_result.dirty if database_result else False,
)
return RefreshResult(
generation=generation,
status=status,
schema=schema,
migrations=migrations,
connection_label=resolved.label if resolved is not None else None,
dialect=_dialect_name(resolved.value) if resolved is not None else None,
diagnostics=tuple(diagnostics),
)
reflect_database
¶
reflect_database(url, *, database_factory=Ormdantic, history_exists=None)
Reflect a live schema and read existing migration history in a worker.
Source code in ormdantic/playground/services.py
def reflect_database(
url: str,
*,
database_factory: Callable[[str], Any] = Ormdantic,
history_exists: Callable[[SchemaSnapshot], bool] | None = None,
) -> DatabaseRefresh:
"""Reflect a live schema and read existing migration history in a worker."""
database = database_factory(url)
live_snapshot = database.migrations.live_snapshot()
if history_exists is not None:
has_history = history_exists(live_snapshot)
else:
has_history = bool(asyncio.run(database.migrations.history_table_exists()))
if not has_history:
return DatabaseRefresh(live_snapshot=live_snapshot)
try:
history = tuple(asyncio.run(database.migrations.history()))
except Exception as exc:
return DatabaseRefresh(
live_snapshot=live_snapshot,
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"database.history_failed",
str(exc),
hint="The schema is available; inspect migration history permissions.",
),
),
)
current = next(
(
entry.revision
for entry in reversed(history)
if entry.status == "applied" and not entry.dirty
),
None,
)
return DatabaseRefresh(
live_snapshot=live_snapshot,
history=history,
current_revision=current,
dirty=any(entry.dirty for entry in history),
)
plan_migration
¶
plan_migration(url, before, after)
Generate drift SQL using the existing migration manager.
Source code in ormdantic/playground/services.py
def plan_migration(
url: str,
before: SchemaSnapshot,
after: SchemaSnapshot,
) -> MigrationPlan:
"""Generate drift SQL using the existing migration manager."""
return Ormdantic(url).migrations.generate_plan(before, after, dialect=url)
Watch events¶
SchemaWatcher polls file metadata and database cadence. It emits coalesced, numbered events.
ormdantic.playground.watcher
¶
Dependency-free schema file and database polling.
WatchReason
¶
Bases: str, Enum
Cause of a requested schema refresh.
WatchEvent
dataclass
¶
WatchEvent(generation, reasons, paths=())
SchemaWatcher
¶
SchemaWatcher(*, root, patterns, database_poll_seconds, debounce_milliseconds, clock=monotonic)
Poll watched files and live-schema cadence without blocking the event loop.
Source code in ormdantic/playground/watcher.py
def __init__(
self,
*,
root: Path,
patterns: tuple[str, ...],
database_poll_seconds: float,
debounce_milliseconds: int,
clock: Callable[[], float] = time.monotonic,
) -> None:
self.root = root.resolve()
self.patterns = patterns
self.database_poll_seconds = database_poll_seconds
self.debounce_seconds = debounce_milliseconds / 1000
self._clock = clock
self._fingerprints: dict[Path, Fingerprint] = {}
self._initialized = False
self._generation = 0
self._paused = False
self._resume_pending = False
self._stopped = False
self._pending_paths: set[Path] = set()
self._pending_since: float | None = None
self._next_database_poll = clock() + database_poll_seconds
pause
¶
pause()
Pause file and database polling.
Source code in ormdantic/playground/watcher.py
def pause(self) -> None:
"""Pause file and database polling."""
self._paused = True
resume
¶
resume()
Resume polling and request one fresh generation.
Source code in ormdantic/playground/watcher.py
def resume(self) -> None:
"""Resume polling and request one fresh generation."""
if self._paused:
self._paused = False
self._resume_pending = True
stop
async
¶
stop()
Request clean run-loop shutdown.
Source code in ormdantic/playground/watcher.py
async def stop(self) -> None:
"""Request clean run-loop shutdown."""
self._stopped = True
poll
¶
poll()
Perform one non-blocking metadata poll.
Source code in ormdantic/playground/watcher.py
def poll(self) -> WatchEvent | None:
"""Perform one non-blocking metadata poll."""
if self._stopped or self._paused:
return None
now = self._clock()
current = self._scan()
if not self._initialized:
self._initialized = True
self._fingerprints = current
self._next_database_poll = now + self.database_poll_seconds
return self._event(
(WatchReason.INITIAL,),
tuple(sorted(current)),
)
if self._resume_pending:
self._resume_pending = False
self._fingerprints = current
self._pending_paths.clear()
self._pending_since = None
self._next_database_poll = now + self.database_poll_seconds
return self._event((WatchReason.RESUMED,), ())
changed = _changed_paths(self._fingerprints, current)
self._fingerprints = current
if changed:
self._pending_paths.update(changed)
if self._pending_since is None:
self._pending_since = now
reasons: list[WatchReason] = []
if (
self._pending_since is not None
and now - self._pending_since >= self.debounce_seconds
):
reasons.append(WatchReason.FILES)
if now >= self._next_database_poll:
reasons.append(WatchReason.DATABASE)
while self._next_database_poll <= now:
self._next_database_poll += self.database_poll_seconds
if not reasons:
return None
paths = (
tuple(sorted(self._pending_paths)) if WatchReason.FILES in reasons else ()
)
if WatchReason.FILES in reasons:
self._pending_paths.clear()
self._pending_since = None
return self._event(tuple(reasons), paths)
run
async
¶
run(emit)
Poll until stopped and publish every coalesced event.
Source code in ormdantic/playground/watcher.py
async def run(
self,
emit: Callable[[WatchEvent], Awaitable[None]],
) -> None:
"""Poll until stopped and publish every coalesced event."""
while not self._stopped:
event = self.poll()
if event is not None:
await emit(event)
if self._stopped:
break
await asyncio.sleep(self._sleep_interval())
Workspace and safety records¶
Workspace functions parse, edit, convert, draft, and atomically save migration artifacts. Safety functions evaluate immutable action requests against current preflight state.
ormdantic.playground.workspace
¶
Migration artifact discovery, editing, drafts, and atomic persistence.
ArtifactDocument
dataclass
¶
ArtifactDocument(path, format, source, artifact, last_valid_artifact=None, diagnostics=(), dirty=False)
One migration file and its current editor state.
MigrationWorkspace
dataclass
¶
MigrationWorkspace(documents=(), selected_path=None)
load_workspace
¶
load_workspace(directory)
Load every top-level TOML and JSON migration without hiding errors.
Source code in ormdantic/playground/workspace.py
def load_workspace(directory: Path) -> MigrationWorkspace:
"""Load every top-level TOML and JSON migration without hiding errors."""
root = directory.resolve()
if not root.is_dir():
return MigrationWorkspace()
paths = sorted(
(
path
for path in root.iterdir()
if path.is_file() and path.suffix.casefold() in {".toml", ".json"}
),
key=lambda path: path.name,
)
documents = [_load_document(path) for path in paths]
documents = _annotate_revision_graph(documents)
return MigrationWorkspace(documents=tuple(documents))
select_document
¶
select_document(workspace, path)
Select a known document without mutating the workspace.
Source code in ormdantic/playground/workspace.py
def select_document(
workspace: MigrationWorkspace,
path: Path,
) -> MigrationWorkspace:
"""Select a known document without mutating the workspace."""
resolved = path.resolve()
if not any(document.path == resolved for document in workspace.documents):
raise ValueError(f"migration document is not in the workspace: {path}")
return replace(workspace, selected_path=resolved)
update_source
¶
update_source(document, source)
Parse an edited TOML source while retaining the last valid artifact.
Source code in ormdantic/playground/workspace.py
def update_source(document: ArtifactDocument, source: str) -> ArtifactDocument:
"""Parse an edited TOML source while retaining the last valid artifact."""
if document.format == "json":
raise ValueError("Convert to TOML before editing a JSON migration artifact")
last_valid = document.artifact or document.last_valid_artifact
try:
artifact = _parse_edited_source(source, document.format)
except Exception as exc:
return replace(
document,
source=source,
artifact=None,
last_valid_artifact=last_valid,
diagnostics=(
Diagnostic.create(
Severity.ERROR,
"artifact.edit_invalid",
str(exc),
source=str(document.path),
hint="Fix the TOML error before saving or running this migration.",
),
),
dirty=True,
)
return replace(
document,
source=source,
artifact=artifact,
last_valid_artifact=artifact,
diagnostics=(),
dirty=True,
)
replace_operation_sql
¶
replace_operation_sql(document, *, index, sql, rollback=False)
Replace one SQL operation and regenerate artifact checksum/source.
Source code in ormdantic/playground/workspace.py
def replace_operation_sql(
document: ArtifactDocument,
*,
index: int,
sql: str,
rollback: bool = False,
) -> ArtifactDocument:
"""Replace one SQL operation and regenerate artifact checksum/source."""
artifact = document.artifact
if artifact is None:
raise ValueError("Fix artifact validation errors before editing operation SQL")
attribute = "rollback_operations" if rollback else "operations"
operations = [
operation_from_dict(operation_to_dict(operation))
for operation in getattr(artifact, attribute)
]
try:
payload = operation_to_dict(operations[index])
except IndexError as exc:
raise IndexError(f"operation index out of range: {index}") from exc
payload["sql"] = sql
operations[index] = operation_from_dict(payload)
updated = replace(artifact, **{attribute: operations}).with_checksum()
source = updated.to_toml() if document.format == "toml" else updated.to_json()
return replace(
document,
source=source,
artifact=updated,
last_valid_artifact=updated,
diagnostics=(),
dirty=True,
)
convert_to_toml
¶
convert_to_toml(document, destination, *, overwrite=False)
Save a valid JSON artifact as a separate canonical TOML document.
Source code in ormdantic/playground/workspace.py
def convert_to_toml(
document: ArtifactDocument,
destination: Path,
*,
overwrite: bool = False,
) -> ArtifactDocument:
"""Save a valid JSON artifact as a separate canonical TOML document."""
if document.artifact is None:
raise ValueError("Cannot convert an invalid migration artifact")
artifact = document.artifact.with_checksum()
converted = ArtifactDocument(
path=destination.resolve(),
format="toml",
source=artifact.to_toml(),
artifact=artifact,
last_valid_artifact=artifact,
dirty=True,
)
return save_document(converted, overwrite=overwrite)
save_document
¶
save_document(document, *, destination=None, overwrite=False)
Atomically save a valid document with a current checksum.
Source code in ormdantic/playground/workspace.py
def save_document(
document: ArtifactDocument,
*,
destination: Path | None = None,
overwrite: bool = False,
) -> ArtifactDocument:
"""Atomically save a valid document with a current checksum."""
artifact = document.artifact
if artifact is None:
raise ValueError("Fix artifact validation errors before saving")
path = (destination or document.path).resolve()
if path.exists() and path != document.path and not overwrite:
raise FileExistsError(path)
format_value: ArtifactFormat = (
"json" if path.suffix.casefold() == ".json" else "toml"
)
artifact = artifact.with_checksum()
source = artifact.to_json() if format_value == "json" else artifact.to_toml()
_atomic_write(path, source)
return ArtifactDocument(
path=path,
format=format_value,
source=source,
artifact=artifact,
last_valid_artifact=artifact,
dirty=False,
)
draft_path
¶
draft_path(root, document)
Return the project-local recovery path for a document.
Source code in ormdantic/playground/workspace.py
def draft_path(root: Path, document: ArtifactDocument) -> Path:
"""Return the project-local recovery path for a document."""
artifact = document.artifact or document.last_valid_artifact
identifier = artifact.revision if artifact is not None else document.path.stem
safe_identifier = re.sub(r"[^A-Za-z0-9_.-]+", "_", identifier).strip(".")
if not safe_identifier:
safe_identifier = "untitled"
return root.resolve() / ".ormdantic" / "drafts" / f"{safe_identifier}.toml"
write_draft
¶
write_draft(root, document)
Atomically persist the current editor source for crash recovery.
Source code in ormdantic/playground/workspace.py
def write_draft(root: Path, document: ArtifactDocument) -> Path:
"""Atomically persist the current editor source for crash recovery."""
path = draft_path(root, document)
_atomic_write(path, document.source)
return path
recover_draft
¶
recover_draft(root, document)
Load the current recovery source back into the editor document.
Source code in ormdantic/playground/workspace.py
def recover_draft(root: Path, document: ArtifactDocument) -> ArtifactDocument:
"""Load the current recovery source back into the editor document."""
path = draft_path(root, document)
source = path.read_text(encoding="utf-8")
if document.format == "json":
document = replace(
document, format="toml", path=document.path.with_suffix(".toml")
)
return update_source(document, source)
discard_draft
¶
discard_draft(root, document)
Remove a recovery draft when the caller has confirmed the action.
Source code in ormdantic/playground/workspace.py
def discard_draft(root: Path, document: ArtifactDocument) -> None:
"""Remove a recovery draft when the caller has confirmed the action."""
try:
draft_path(root, document).unlink()
except FileNotFoundError:
return
ormdantic.playground.safety
¶
Pure preflight and confirmation policy for playground actions.
Risk
¶
Bases: str, Enum
Mutation risk presented to users and confirmation policy.
ActionRequest
dataclass
¶
ActionRequest(action, environment, database_name, target, risk, sql=(), destructive_sql=(), artifact_checksum=None, reviewed_generation=None)
Immutable identity and reviewed SQL for one requested action.
PreflightContext
dataclass
¶
PreflightContext(connected, target_imported, dialect, artifact_dialect, history_readable, history_dirty, artifact_valid, checksum_valid, dependencies_valid, revision_state_valid, rollback_available, snapshot_current, operations_supported, operation_running, editor_valid, editor_dirty, sql_present, destructive_reviewed, artifact_checksum, generation)
Current state against which an action review is validated.
SafetyDecision
dataclass
¶
SafetyDecision(allowed, phrase, reasons=())
classify_plan
¶
classify_plan(plan)
Classify migration SQL using authoritative plan metadata.
Source code in ormdantic/playground/safety.py
def classify_plan(plan: MigrationPlan) -> Risk:
"""Classify migration SQL using authoritative plan metadata."""
if plan.has_destructive_operations:
return Risk.DESTRUCTIVE
if plan.operations:
return Risk.WRITE
return Risk.READ_ONLY
evaluate_action
¶
evaluate_action(request, environment, context, *, confirmed=False, confirmation=None)
Evaluate preflight and the non-cacheable confirmation for one action.
Source code in ormdantic/playground/safety.py
def evaluate_action(
request: ActionRequest,
environment: EnvironmentConfig,
context: PreflightContext,
*,
confirmed: bool = False,
confirmation: str | None = None,
) -> SafetyDecision:
"""Evaluate preflight and the non-cacheable confirmation for one action."""
reasons = list(_preflight_reasons(request, environment, context))
phrase = _confirmation_phrase(request, environment)
if request.risk is not Risk.READ_ONLY:
if not confirmed:
reasons.append(
f"Confirm {request.action} for {request.environment}:{request.target}"
)
if phrase is not None and confirmation != phrase:
reasons.append(f"Type the exact confirmation phrase: {phrase}")
return SafetyDecision(
allowed=not reasons,
phrase=phrase,
reasons=tuple(reasons),
)
Controller and operations¶
PlaygroundController owns immutable state and exposes intents. MigrationOperations enforces approved request identity before calling the migration manager.
ormdantic.playground.controller
¶
State and intent coordinator for the playground application.
ControllerActionOutcome
dataclass
¶
ControllerActionOutcome(decision, executed, result=None, error=None)
PlaygroundController
¶
PlaygroundController(config, *, refresh_service=None, operations=None)
Own application state and make services available through safe intents.
Source code in ormdantic/playground/controller.py
def __init__(
self,
config: EffectiveConfig,
*,
refresh_service: Any | None = None,
operations: Any | None = None,
) -> None:
self.config = config
self.refresh_service = refresh_service or RefreshService()
self.operations = operations or MigrationOperations(config)
self.workspace = load_workspace(config.project.migrations_dir)
self.state = PlaygroundState(
environment=config.environment.name,
migrations=_migration_state(self.workspace),
)
self._refresh_lock = asyncio.Lock()
self._subscribers: list[Callable[[PlaygroundState], None]] = []
state
instance-attribute
¶
state = PlaygroundState(environment=name, migrations=_migration_state(workspace))
subscribe
¶
subscribe(callback)
Publish future state snapshots and return an unsubscribe callback.
Source code in ormdantic/playground/controller.py
def subscribe(
self,
callback: Callable[[PlaygroundState], None],
) -> Callable[[], None]:
"""Publish future state snapshots and return an unsubscribe callback."""
self._subscribers.append(callback)
def unsubscribe() -> None:
try:
self._subscribers.remove(callback)
except ValueError:
return
return unsubscribe
refresh
async
¶
refresh(*, generation=None)
Run and publish a generation-safe schema refresh.
Source code in ormdantic/playground/controller.py
async def refresh(self, *, generation: int | None = None) -> PlaygroundState:
"""Run and publish a generation-safe schema refresh."""
async with self._refresh_lock:
next_generation = max(self.state.generation + 1, generation or 0)
self.state = replace(
self.state,
generation=next_generation,
status=RefreshStatus.RUNNING,
)
self._publish()
result = await self.refresh_service.refresh(
self.config,
generation=next_generation,
previous=self.state.schema,
)
updated = accept_refresh(self.state, result)
updated = replace(
updated,
migrations=_migration_state(self.workspace, updated.migrations),
)
self.state = updated
self._publish()
return self.state
reload_workspace
¶
reload_workspace()
Reload migration files while preserving a still-valid selection.
Source code in ormdantic/playground/controller.py
def reload_workspace(self) -> MigrationWorkspace:
"""Reload migration files while preserving a still-valid selection."""
selected = self.workspace.selected_path
workspace = load_workspace(self.config.project.migrations_dir)
if selected is not None and any(
document.path == selected for document in workspace.documents
):
workspace = select_document(workspace, selected)
self.workspace = workspace
self._sync_workspace_state()
return workspace
select_artifact
¶
select_artifact(path)
Select a migration for review and editing.
Source code in ormdantic/playground/controller.py
def select_artifact(self, path: Path) -> None:
"""Select a migration for review and editing."""
if self.workspace.selected_path == path.resolve():
return
self.workspace = select_document(self.workspace, path)
self._sync_workspace_state()
edit_active_source
¶
edit_active_source(source)
Apply a TOML source edit to the selected document.
Source code in ormdantic/playground/controller.py
def edit_active_source(self, source: str) -> ArtifactDocument:
"""Apply a TOML source edit to the selected document."""
active = self._require_active_document()
updated = update_source(active, source)
self._replace_document(updated)
return updated
edit_active_sql
¶
edit_active_sql(index, sql, *, rollback=False)
Apply a selected operation SQL edit.
Source code in ormdantic/playground/controller.py
def edit_active_sql(
self,
index: int,
sql: str,
*,
rollback: bool = False,
) -> ArtifactDocument:
"""Apply a selected operation SQL edit."""
active = self._require_active_document()
updated = replace_operation_sql(
active,
index=index,
sql=sql,
rollback=rollback,
)
self._replace_document(updated)
return updated
save_active
¶
save_active(*, destination=None, overwrite=False)
Atomically persist the selected valid editor document.
Source code in ormdantic/playground/controller.py
def save_active(
self,
*,
destination: Path | None = None,
overwrite: bool = False,
) -> ArtifactDocument:
"""Atomically persist the selected valid editor document."""
saved = save_document(
self._require_active_document(),
destination=destination,
overwrite=overwrite,
)
self._replace_document(saved)
if destination is not None:
self.workspace = replace(self.workspace, selected_path=saved.path)
self._sync_workspace_state()
return saved
convert_active_to_toml
¶
convert_active_to_toml(destination=None, *, overwrite=False)
Convert selected legacy JSON to a separate TOML artifact.
Source code in ormdantic/playground/controller.py
def convert_active_to_toml(
self,
destination: Path | None = None,
*,
overwrite: bool = False,
) -> ArtifactDocument:
"""Convert selected legacy JSON to a separate TOML artifact."""
active = self._require_active_document()
target = destination or active.path.with_suffix(".toml")
converted = convert_to_toml(active, target, overwrite=overwrite)
self.reload_workspace()
self.workspace = select_document(self.workspace, converted.path)
self._sync_workspace_state()
return self._require_active_document()
write_active_draft
¶
write_active_draft()
Persist selected editor source in project-local recovery storage.
Source code in ormdantic/playground/controller.py
def write_active_draft(self) -> Path:
"""Persist selected editor source in project-local recovery storage."""
return write_draft(self.config.root, self._require_active_document())
recover_active_draft
¶
recover_active_draft()
Restore selected editor source from its recovery draft.
Source code in ormdantic/playground/controller.py
def recover_active_draft(self) -> ArtifactDocument:
"""Restore selected editor source from its recovery draft."""
recovered = recover_draft(
self.config.root,
self._require_active_document(),
)
self._replace_document(recovered)
return recovered
discard_active_draft
¶
discard_active_draft()
Discard selected recovery source after UI confirmation.
Source code in ormdantic/playground/controller.py
def discard_active_draft(self) -> None:
"""Discard selected recovery source after UI confirmation."""
discard_draft(self.config.root, self._require_active_document())
build_action_request
¶
build_action_request(action, *, database_name)
Bind an action review to current artifact content and generation.
Source code in ormdantic/playground/controller.py
def build_action_request(
self,
action: str,
*,
database_name: str,
) -> ActionRequest:
"""Bind an action review to current artifact content and generation."""
document = self._require_active_document()
artifact = document.artifact
if artifact is None:
raise ValueError("selected migration artifact is invalid")
if action == "apply":
operations = artifact.operations
plan = MigrationPlan(operations=list(operations))
elif action == "rollback":
operations = artifact.rollback_operations
plan = MigrationPlan(
operations=list(operations),
diff=artifact.diff,
warnings=artifact.warnings,
safety=artifact.safety,
)
else:
raise ValueError(f"unsupported artifact action: {action}")
risk = classify_plan(plan)
return ActionRequest(
action=action,
environment=self.config.environment.name,
database_name=database_name,
target=artifact.revision,
risk=risk,
sql=tuple(operation.sql for operation in operations),
destructive_sql=tuple(
operation.sql for operation in operations if operation.destructive
),
artifact_checksum=artifact.checksum,
reviewed_generation=self.state.generation,
)
generate_migration
async
¶
generate_migration(revision, description=None)
Generate a canonical TOML artifact from the current fresh drift.
Source code in ormdantic/playground/controller.py
async def generate_migration(
self,
revision: str,
description: str | None = None,
) -> Path | None:
"""Generate a canonical TOML artifact from the current fresh drift."""
before = self.state.schema.live_snapshot
after = self.state.schema.model_snapshot
if before is None or after is None or self.state.schema.stale:
raise ValueError("refresh model and live schemas before generating")
if not self.state.schema.forward_sql:
raise ValueError("there is no schema drift to generate")
self.state = replace(
self.state,
operation=OperationState(
name="generate",
target=revision,
running=True,
message=f"Generating {revision}",
),
)
self._publish()
try:
path = await self.operations.generate(
revision,
description,
before=before,
after=after,
depends_on=(
(self.state.migrations.current_revision,)
if self.state.migrations.current_revision
else ()
),
)
except Exception as exc:
self._publish_operation_failure("generate", revision, exc)
raise
if path is not None:
self.reload_workspace()
self.select_artifact(path)
self.state = replace(
self.state,
operation=OperationState(
name="generate",
target=revision,
message=(
f"Generated {revision}"
if path is not None
else "No migration generated"
),
),
)
self._publish()
return path
build_repair_request
¶
build_repair_request(revision, *, database_name)
Bind a dirty-history repair to the selected row and generation.
Source code in ormdantic/playground/controller.py
def build_repair_request(
self,
revision: str,
*,
database_name: str,
) -> ActionRequest:
"""Bind a dirty-history repair to the selected row and generation."""
entry = next(
(
item
for item in self.state.migrations.history
if item.revision == revision
),
None,
)
if entry is None:
raise ValueError(f"history revision is not available: {revision}")
if not entry.dirty:
raise ValueError(f"history revision is not dirty: {revision}")
return ActionRequest(
action="repair",
environment=self.config.environment.name,
database_name=database_name,
target=revision,
risk=Risk.HISTORY_REWRITE,
sql=(f"clear dirty flag; preserve status {entry.status}",),
reviewed_generation=self.state.generation,
)
execute_repair
async
¶
execute_repair(request, context, *, status, confirmed, confirmation)
Repair one dirty history row after an exact typed review.
Source code in ormdantic/playground/controller.py
async def execute_repair(
self,
request: ActionRequest,
context: PreflightContext,
*,
status: str,
confirmed: bool,
confirmation: str | None,
) -> ControllerActionOutcome:
"""Repair one dirty history row after an exact typed review."""
decision = evaluate_action(
request,
self.config.environment,
context,
confirmed=confirmed,
confirmation=confirmation,
)
if not decision.allowed:
return ControllerActionOutcome(decision=decision, executed=False)
self.state = replace(
self.state,
operation=OperationState(
name="repair",
target=request.target,
running=True,
message=f"Repairing {request.target}",
),
)
self._publish()
try:
result = await self.operations.repair(
request.target,
status,
request,
decision,
)
except Exception as exc:
diagnostic = self._publish_operation_failure(
"repair",
request.target,
exc,
)
return ControllerActionOutcome(
decision=decision,
executed=True,
error=diagnostic,
)
await self.refresh()
self.state = replace(
self.state,
operation=OperationState(
name="repair",
target=request.target,
message=f"Repaired {request.target}",
),
)
self._publish()
return ControllerActionOutcome(decision, True, result)
build_squash_request
¶
build_squash_request(paths, revision, *, database_name)
Bind a local squash preview to exact pending artifact checksums.
Source code in ormdantic/playground/controller.py
def build_squash_request(
self,
paths: tuple[Path, ...],
revision: str,
*,
database_name: str,
) -> ActionRequest:
"""Bind a local squash preview to exact pending artifact checksums."""
documents = self._squash_documents(paths)
artifacts = cast(
tuple[MigrationArtifact, ...],
tuple(document.artifact for document in documents),
)
sql = tuple(
operation.sql for artifact in artifacts for operation in artifact.operations
)
return ActionRequest(
action="squash",
environment=self.config.environment.name,
database_name=database_name,
target=revision,
risk=Risk.HISTORY_REWRITE,
sql=sql,
destructive_sql=tuple(
operation.sql
for artifact in artifacts
for operation in artifact.operations
if operation.destructive
),
artifact_checksum=_workspace_checksum(documents),
reviewed_generation=self.state.generation,
)
execute_squash
async
¶
execute_squash(request, context, paths, *, confirmed, confirmation)
Write a squashed artifact only if the reviewed inputs are unchanged.
Source code in ormdantic/playground/controller.py
async def execute_squash(
self,
request: ActionRequest,
context: PreflightContext,
paths: tuple[Path, ...],
*,
confirmed: bool,
confirmation: str | None,
) -> ControllerActionOutcome:
"""Write a squashed artifact only if the reviewed inputs are unchanged."""
decision = evaluate_action(
request,
self.config.environment,
context,
confirmed=confirmed,
confirmation=confirmation,
)
if not decision.allowed:
return ControllerActionOutcome(decision=decision, executed=False)
documents = self._squash_documents(paths)
if request.artifact_checksum != _workspace_checksum(documents):
decision = replace(
decision,
allowed=False,
reasons=(*decision.reasons, "squash inputs changed after review"),
)
return ControllerActionOutcome(decision=decision, executed=False)
self.state = replace(
self.state,
operation=OperationState(
name="squash",
target=request.target,
running=True,
message=f"Squashing into {request.target}",
),
)
self._publish()
try:
path = await self.operations.squash(
paths,
request.target,
request,
decision,
)
except Exception as exc:
diagnostic = self._publish_operation_failure(
"squash",
request.target,
exc,
)
return ControllerActionOutcome(
decision=decision,
executed=True,
error=diagnostic,
)
self.reload_workspace()
self.select_artifact(path)
self.state = replace(
self.state,
operation=OperationState(
name="squash",
target=request.target,
message=f"Created squash {request.target}",
),
)
self._publish()
return ControllerActionOutcome(decision, True, path)
execute_action
async
¶
execute_action(request, context, *, confirmed=False, confirmation=None)
Evaluate, execute, refresh, and only then report completion.
Source code in ormdantic/playground/controller.py
async def execute_action(
self,
request: ActionRequest,
context: PreflightContext,
*,
confirmed: bool = False,
confirmation: str | None = None,
) -> ControllerActionOutcome:
"""Evaluate, execute, refresh, and only then report completion."""
decision = evaluate_action(
request,
self.config.environment,
context,
confirmed=confirmed,
confirmation=confirmation,
)
if not decision.allowed:
return ControllerActionOutcome(decision=decision, executed=False)
document = self._require_active_document()
self.state = replace(
self.state,
operation=OperationState(
name=request.action,
target=request.target,
running=True,
message=f"Running {request.action} for {request.target}",
),
)
self._publish()
try:
if request.action == "apply":
result = await self.operations.apply(document, request, decision)
elif request.action == "rollback":
result = await self.operations.rollback(document, request, decision)
else:
raise ValueError(f"unsupported controller action: {request.action}")
except Exception as exc:
diagnostic = self._publish_operation_failure(
request.action,
request.target,
exc,
)
return ControllerActionOutcome(
decision=decision,
executed=True,
error=diagnostic,
)
self.reload_workspace()
await self.refresh()
verb = "Applied" if request.action == "apply" else "Rolled back"
self.state = replace(
self.state,
operation=OperationState(
name=request.action,
target=request.target,
running=False,
message=f"{verb} {request.target}",
),
)
self._publish()
return ControllerActionOutcome(
decision=decision,
executed=True,
result=result,
)
ormdantic.playground.operations
¶
Authorized migration operations shared by the playground UI.
MigrationOperations
¶
MigrationOperations(config, *, url_resolver=resolve_database_url, database_factory=Ormdantic, thread_runner=to_thread, artifact_generator=None)
Run migration APIs in worker threads after exact safety authorization.
Source code in ormdantic/playground/operations.py
def __init__(
self,
config: EffectiveConfig,
*,
url_resolver: Callable[..., DatabaseUrlSource] = resolve_database_url,
database_factory: Callable[[str], Any] = Ormdantic,
thread_runner: Callable[..., Any] = asyncio.to_thread,
artifact_generator: Callable[..., MigrationArtifact | None] | None = None,
) -> None:
self.config = config
self._url_resolver = url_resolver
self._database_factory = database_factory
self._thread_runner = thread_runner
self._artifact_generator = artifact_generator or _generate_artifact
generate
async
¶
generate(revision, description, *, before, after, depends_on=())
Generate and atomically save a canonical TOML artifact.
Source code in ormdantic/playground/operations.py
async def generate(
self,
revision: str,
description: str | None,
*,
before: SchemaSnapshot,
after: SchemaSnapshot,
depends_on: Sequence[str] = (),
) -> Path | None:
"""Generate and atomically save a canonical TOML artifact."""
_validate_revision(revision)
resolved = self._url_resolver(self.config.environment)
generate = partial(
self._artifact_generator,
revision,
before,
after,
dialect=resolved.value,
description=description,
depends_on=tuple(depends_on),
)
artifact = await self._thread_runner(generate)
if artifact is None:
return None
destination = self.config.project.migrations_dir / f"{revision}.toml"
document = ArtifactDocument(
path=destination,
format="toml",
source=artifact.to_toml(),
artifact=artifact,
last_valid_artifact=artifact,
dirty=True,
)
return save_document(document).path
apply
async
¶
apply(document, request, decision)
Apply the exact artifact covered by an allowed safety decision.
Source code in ormdantic/playground/operations.py
async def apply(
self,
document: ArtifactDocument,
request: ActionRequest,
decision: SafetyDecision,
) -> bool:
"""Apply the exact artifact covered by an allowed safety decision."""
artifact = _authorized_artifact(document, request, decision, "apply")
risk = classify_plan(artifact.to_plan())
_validate_risk(request.risk, risk)
resolved = self._url_resolver(self.config.environment)
return await self._thread_runner(
_apply_sync,
self._database_factory,
resolved.value,
artifact,
risk is Risk.DESTRUCTIVE,
)
rollback
async
¶
rollback(document, request, decision)
Roll back the exact artifact covered by an allowed decision.
Source code in ormdantic/playground/operations.py
async def rollback(
self,
document: ArtifactDocument,
request: ActionRequest,
decision: SafetyDecision,
) -> bool:
"""Roll back the exact artifact covered by an allowed decision."""
artifact = _authorized_artifact(document, request, decision, "rollback")
rollback_plan = MigrationPlan(
operations=list(artifact.rollback_operations),
diff=artifact.diff,
warnings=artifact.warnings,
safety=artifact.safety,
)
risk = classify_plan(rollback_plan)
_validate_risk(request.risk, risk)
resolved = self._url_resolver(self.config.environment)
return await self._thread_runner(
_rollback_sync,
self._database_factory,
resolved.value,
artifact,
risk is Risk.DESTRUCTIVE,
)
repair
async
¶
repair(revision, status, request, decision)
Repair selected history metadata after authorization.
Source code in ormdantic/playground/operations.py
async def repair(
self,
revision: str,
status: str,
request: ActionRequest,
decision: SafetyDecision,
) -> int:
"""Repair selected history metadata after authorization."""
_authorized_request(request, decision, "repair", revision)
resolved = self._url_resolver(self.config.environment)
return await self._thread_runner(
_repair_sync,
self._database_factory,
resolved.value,
revision,
status,
)
squash
async
¶
squash(paths, revision, request, decision)
Squash a reviewed contiguous selection into a new TOML artifact.
Source code in ormdantic/playground/operations.py
async def squash(
self,
paths: Sequence[Path],
revision: str,
request: ActionRequest,
decision: SafetyDecision,
) -> Path:
"""Squash a reviewed contiguous selection into a new TOML artifact."""
_validate_revision(revision)
_authorized_request(request, decision, "squash", revision)
artifacts = tuple(MigrationArtifact.read(path) for path in paths)
resolved = self._url_resolver(self.config.environment)
squashed = await self._thread_runner(
_squash_sync,
self._database_factory,
resolved.value,
revision,
artifacts,
)
destination = self.config.project.migrations_dir / f"{revision}.toml"
document = ArtifactDocument(
path=destination,
format="toml",
source=squashed.to_toml(),
artifact=squashed,
last_valid_artifact=squashed,
dirty=True,
)
return save_document(document).path
history
async
¶
history()
Read durable history in the blocking worker boundary.
Source code in ormdantic/playground/operations.py
async def history(self) -> tuple[MigrationHistoryEntry, ...]:
"""Read durable history in the blocking worker boundary."""
resolved = self._url_resolver(self.config.environment)
return await self._thread_runner(
_history_sync,
self._database_factory,
resolved.value,
)