···13131414 @classmethod
1515 def parse(cls, uri: str) -> "AtUri":
1616- parts = uri.split("/")
1717- return cls(parts[2], parts[3], parts[4])
1616+ if not uri.startswith("at://"):
1717+ raise ValueError(f"not an AT URI: {uri!r}")
1818+ parts = uri[5:].split("/")
1919+ if len(parts) != 3 or not all(parts):
2020+ raise ValueError(f"malformed AT URI: {uri!r}")
2121+ return cls(*parts)
18221923 def __str__(self) -> str:
2024 return f"at://{self.did}/{self.collection}/{self.rkey}"
+6-4
core/util.py
···445566def now_iso() -> str:
77- """Current UTC timestamp in ISO format."""
88- return datetime.now(timezone.utc).isoformat()
77+ """Current UTC timestamp in ISO format with Z suffix (ATProto convention)."""
88+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
9910101111def format_datetime_utc(value: str) -> str:
···22222323def blob_url(pds: str, did: str, cid: str) -> str:
2424 """Construct an ATProto blob fetch URL."""
2525- return f"{pds}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}"
2525+ return f"{pds.rstrip('/')}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}"
262627272828def attachment_cid(attachment: dict) -> str:
2929 """Return the blob CID from a post attachment, or empty string if missing."""
3030- return (attachment.get("file") or {}).get("ref", {}).get("$link", "")
3030+ file = attachment.get("file") or {}
3131+ ref = file.get("ref") or {}
3232+ return ref.get("$link") or ""