Backend messages and field descriptions that reach the UI lose their em-dashes

Per the Microcopy rule: size and duration errors, the flavor errors, run
parameter errors, the Resources field descriptions, 401/409/400 details,
secret errors, the websocket node description and its on_connect setting.
The generated client follows.
This commit is contained in:
2026-10-27 17:52:55 +01:00
parent 4cb028de41
commit e37aa2bdd7
12 changed files with 39 additions and 39 deletions
+1 -1
View File
@@ -527,7 +527,7 @@ def get_current_user(request: Request, session: SessionDep, token: TokenDep) ->
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This session is no longer valid — please log in again",
detail="This session is no longer valid. Please log in again",
)
return user
+1 -1
View File
@@ -182,7 +182,7 @@ async def discard_dashboard_draft(name: str, store: DashboardStoreDep) -> Any:
raise HTTPException(
status_code=400,
detail=(
f"Dashboard '{name}' has never been published — delete it instead "
f"Dashboard '{name}' has never been published. Delete it instead "
"of discarding it"
),
)
+2 -2
View File
@@ -303,7 +303,7 @@ def _require_enabled(controller: FlowController, name: str) -> None:
if not controller.is_enabled(name):
raise HTTPException(
status_code=409,
detail=f"Flow '{name}' is stopped — start it before running it",
detail=f"Flow '{name}' is stopped. Start it before running it",
)
@@ -601,7 +601,7 @@ async def discard_draft(name: str, controller: FlowControllerDep) -> Any:
raise HTTPException(
status_code=400,
detail=(
f"Flow '{name}' has never been published — delete it instead of "
f"Flow '{name}' has never been published. Delete it instead of "
"discarding it"
),
)
+3 -3
View File
@@ -358,7 +358,7 @@ NODE_TYPES: dict[str, NodeType] = {
),
"websocket": NodeType(
title="WebSocket",
description="Listen on a socket, or send to it — as a client or a server.",
description="Listen on a socket, or send to it, as a client or a server.",
cls=WebSocketNode,
params_schema=_schema_of(WebSocketNode),
),
@@ -2133,8 +2133,8 @@ def _collect_issues(
ValidationIssue(
code="unauthenticated_hook",
message=(
f"Hook '{entry.node.local_id}' has no secret — "
"anyone who knows its URL can reach it."
f"Hook '{entry.node.local_id}' has no secret. "
"Anyone who knows its URL can reach it."
),
flow=entry.flow,
node=entry.id,
+1 -1
View File
@@ -256,7 +256,7 @@ class MessageSpec(BaseModel):
raise TypeError(
f"{where}: {stray} cannot travel as JSON. A subset with nothing "
"in it, or a division that had no denominator, is what usually "
"produces one — publish None, or a number that says so."
"produces one. Publish None, or a number that says so."
)
if self.dtype is DType.SERIES:
ok = _is_series(value)
+1 -1
View File
@@ -91,7 +91,7 @@ class WebSocketNode(Node):
on_connect: list[Any] = Field(
default_factory=list,
description=(
"Frames sent after every (re)connect, client only — the "
"Frames sent after every (re)connect, client only: the "
"subscribe or auth message many APIs expect. A string goes "
"raw, anything else as JSON; an item may be a secret reference."
),
+1 -1
View File
@@ -299,7 +299,7 @@ def resolve_flavor(wanted: Resources) -> Resources:
row = session.get(Flavor, wanted.flavor)
if row is None:
raise UnknownFlavor(
f"flavor '{wanted.flavor}' does not exist — `fluksio flavors` lists them"
f"flavor '{wanted.flavor}' does not exist. `fluksio flavors` lists them"
)
# Built fresh rather than copied, so the flavor-and-numbers check runs on
# the result and this cannot quietly produce something invalid.
+2 -2
View File
@@ -282,8 +282,8 @@ def _from_run(
run_id, _, output = spelling.partition(".")
if not run_id or not output:
raise RunRejected(
f"Parameter '{key}': '{RUN_REF_PREFIX}{spelling}' names no output — "
f"write {RUN_REF_PREFIX}<run id>.<output>"
f"Parameter '{key}': '{RUN_REF_PREFIX}{spelling}' names no output. "
f"Write {RUN_REF_PREFIX}<run id>.<output>"
)
run = session.get(Run, run_id)
if run is None:
+11 -11
View File
@@ -38,7 +38,7 @@ def _megabytes(value: Any) -> Any:
return value
match = _SIZE.match(value)
if not match:
raise ValueError(f"'{value}' is not a size — write 512M, 2G, or 512")
raise ValueError(f"'{value}' is not a size. Write 512M, 2G, or 512")
amount, unit = int(match.group(1)), (match.group(2) or "m").lower()
return amount * 1024 if unit.startswith("g") else amount
@@ -49,7 +49,7 @@ def _seconds(value: Any) -> Any:
return value
match = _SPAN.match(value)
if not match:
raise ValueError(f"'{value}' is not a duration — write 90s, 30m, 2h, or 90")
raise ValueError(f"'{value}' is not a duration. Write 90s, 30m, 2h, or 90")
return int(match.group(1)) * _SPAN_SECONDS[(match.group(2) or "s").lower()]
@@ -95,7 +95,7 @@ class Resources(BaseModel):
description=(
"Megabytes held for the whole execution; accepts '512M' or '2G'. "
"Counted against machines that said how much they have, and "
"ignored by those that did not — which is a machine with nothing "
"ignored by those that did not, which is a machine with nothing "
"to say about memory, not one with none."
),
)
@@ -113,15 +113,15 @@ class Resources(BaseModel):
description=(
"How long one execution is expected to take; accepts '30m' or "
"'2h'. A statement about the node for whoever is planning around "
"it, not a limit — the limit is `timeout`."
"it, not a limit. The limit is `timeout`."
),
)
env: dict[str, str] = Field(
default_factory=dict,
description=(
"Extra environment for the worker this node runs in, applied over "
"what the allocation derives. Where a library's own tuning goes — "
"XLA_FLAGS, XLA_PYTHON_CLIENT_MEM_FRACTION — since those are "
"what the allocation derives. Where a library's own tuning goes "
"(XLA_FLAGS, XLA_PYTHON_CLIENT_MEM_FRACTION), since those are "
"composed strings the engine must not invent."
),
)
@@ -140,8 +140,8 @@ class Resources(BaseModel):
"""
if self.flavor and (self.cpus != 1 or self.gpus != 0 or self.ram is not None):
raise ValueError(
f"flavor '{self.flavor}' already says how much — "
"drop cpus, gpus and ram, or drop the flavor"
f"flavor '{self.flavor}' already says how much. "
"Drop cpus, gpus and ram, or drop the flavor"
)
return self
@@ -203,8 +203,8 @@ class NodeDef(BaseModel):
resources: Resources | None = Field(
default=None,
description=(
"What one execution of this node holds while it runs. Absent — the "
"default — means it is not accounted for and shares the engine's "
"What one execution of this node holds while it runs. Absent (the "
"default) means it is not accounted for and shares the engine's "
"workers, which is right for everything that is not compute-heavy."
),
)
@@ -222,7 +222,7 @@ class NodeDef(BaseModel):
description=(
"What those modules hashed to at sync. The engine hashes them "
"again when a run is claimed, and falls back to this when it "
"cannot see the files — a worker on another machine."
"cannot see the files, as a worker on another machine does."
),
)
+2 -2
View File
@@ -31,7 +31,7 @@ class SecretNotFound(KeyError):
self.name = name
def __str__(self) -> str:
return f"No secret named '{self.name}' — add it under Secrets."
return f"No secret named '{self.name}'. Add it under Secrets."
def fernet(key: str) -> Fernet:
@@ -60,7 +60,7 @@ class SecretsStore:
# flows coming up with silently missing credentials are far harder
# to diagnose than a node that reports why it did not load.
raise SecretsUnreadable(
f"Cannot decrypt {self._path} — it was written with a "
f"Cannot decrypt {self._path}. It was written with a "
"different SECRET_KEY. Re-encrypt it with "
"scripts/rotate_secret_key.py."
) from exc