refactor(secrets): epoch timestamps, a named list request, and unit tests

`SecretInfo` carries `created_at_millis` / `updated_at_millis` as `i64` rather
than RFC 3339 strings, matching `created_at_millis` elsewhere in the platform.
A caller comparing two timestamps no longer parses anything, and the PyO3 layer
hands Python integers instead of stringifying them. `updated_at_millis` is the
only observable that a rotation landed -- no API returns a credential -- so it
is worth being a number a caller can compare.

`RemoteListSecretsRequest` is declared alongside its response instead of being
an inline object built field by field, so a reader of one finds the other.

`function.rs` gains unit tests for the pure parts that had none: canonical JSON
sorts keys at every depth and leaves array order alone -- it is what the version
hash is taken over, so both matter -- floats are refused at any depth, and the
unknown-key check inspects only the level it is handed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfmeJ533rQDnPBkMtjerV6
This commit is contained in:
Jonathan M Hsieh
2026-09-11 18:55:35 +00:00
co-authored by Claude Opus 5
parent 82cf9f3b96
commit d6bcfe52a9
6 changed files with 130 additions and 33 deletions
+7 -2
View File
@@ -2444,8 +2444,13 @@ class AsyncConnection(object):
async def describe_secret(self, name: str) -> SecretInfo:
"""What this database records about a Secret. Never the value."""
return SecretInfo.from_json(
await self._inner.describe_secret(validate_secret_name(name))
name, created_at_millis, updated_at_millis = await self._inner.describe_secret(
validate_secret_name(name)
)
return SecretInfo(
name=name,
created_at_millis=created_at_millis,
updated_at_millis=updated_at_millis,
)
async def list_jobs(self) -> List[JobInfo]:
+21 -16
View File
@@ -116,12 +116,12 @@ class SecretInfo:
[DBConnection.describe_secret][lancedb.db.DBConnection.describe_secret].
"""
__slots__ = ("_name", "_created_at", "_updated_at")
__slots__ = ("_name", "_created_at_millis", "_updated_at_millis")
def __init__(self, name: str, created_at: str, updated_at: str):
def __init__(self, name: str, created_at_millis: int, updated_at_millis: int):
self._name = name
self._created_at = created_at
self._updated_at = updated_at
self._created_at_millis = created_at_millis
self._updated_at_millis = updated_at_millis
@property
def name(self) -> str:
@@ -129,35 +129,40 @@ class SecretInfo:
return self._name
@property
def created_at(self) -> str:
"""When the Secret was created, as an RFC 3339 timestamp."""
return self._created_at
def created_at_millis(self) -> int:
"""When the Secret was created, in milliseconds since the Unix epoch."""
return self._created_at_millis
@property
def updated_at(self) -> str:
"""When the Secret's value was last rotated, as an RFC 3339 timestamp."""
return self._updated_at
def updated_at_millis(self) -> int:
"""When the Secret's value was last rotated, in epoch milliseconds.
The only observable that a rotation landed: no API returns a credential,
so a caller confirms ``alter_secret`` took effect by watching this move.
"""
return self._updated_at_millis
@classmethod
def from_json(cls, value: dict) -> "SecretInfo":
return cls(
name=value["name"],
created_at=value["created_at"],
updated_at=value["updated_at"],
created_at_millis=value["created_at_millis"],
updated_at_millis=value["updated_at_millis"],
)
def __repr__(self) -> str:
return (
f"SecretInfo(name={self._name!r}, created_at={self._created_at!r}, "
f"updated_at={self._updated_at!r})"
f"SecretInfo(name={self._name!r}, "
f"created_at_millis={self._created_at_millis!r}, "
f"updated_at_millis={self._updated_at_millis!r})"
)
def __eq__(self, other: object) -> bool:
return (
isinstance(other, SecretInfo)
and other._name == self._name
and other._created_at == self._created_at
and other._updated_at == self._updated_at
and other._created_at_millis == self._created_at_millis
and other._updated_at_millis == self._updated_at_millis
)
+4 -7
View File
@@ -740,17 +740,14 @@ impl Connection {
})
}
/// Name and timestamps as a plain mapping. `SecretInfo` carries no value,
/// so there is none to filter out here.
/// Name and timestamps as a plain tuple. `SecretInfo` carries no value, so
/// there is none to filter out here. Timestamps stay integers rather than
/// going through a string, so the caller can compare two without parsing.
pub fn describe_secret(self_: PyRef<'_, Self>, name: String) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
future_into_py(self_.py(), async move {
let info = inner.describe_secret(name).await.infer_error()?;
Ok(HashMap::from([
("name".to_string(), info.name),
("created_at".to_string(), info.created_at),
("updated_at".to_string(), info.updated_at),
]))
Ok((info.name, info.created_at_millis, info.updated_at_millis))
})
}