Centralized user messages in Python apps#
Helpful errors with error codes. Do you love it too? During the development of the Snippets, an online preview tool for reStructuredText (and Markdown coming soon), I tried to produce actually helpful messages to the API user. I started with collecting text strings that could appear to users across the codebase and put them into a single file.
Requirements#
I want
have a single central file for all messages
messages are not strictly error but all messages that could ever appear to the user
every message will need at least a title, a brief summary, and, optionally, a longer description
assign codes to messages so the user and developer can quickly “tell” the message even if the wording has changed
consume message as
str
primitive and asdict
The Message
class#
In the project root, I create messages.py
.
Instead of plain constants, I create a simple class with a title, description, and code.
Python 3.7 has introduced dataclasses, which are ideal for the purpose. I want also some nice string representation.
from dataclasses import asdict, dataclass
from typing import Optional
@dataclass
class Message:
code: int
title: str
description: Optional[str] = None
def to_dict(self):
return asdict(self)
def __str__(self):
return (
f"{self.title}: {self.description} (code #{self.code})"
if self.description
else f"{self.title}: (code #{self.code})"
)
Message collection#
Continuing in the messages.py
, the collected strings are now Message
instances:
BookNotFoundMsg = Message(100, "No such book found")
RouteNotFoundMsg = Message(
101,
"No such route",
"The API doesn't know endpoint of such URI."
)
ShareNotFoundMsg = Message(
102,
"Share not found",
"Share with specified digest has not been found."
)
Thanks to Dataclass, we get the appropriate __init__()
constructor for free.
Usage#
String primitives#
Thanks to customized __str__()
, a message will become beautiful string:
from messages import BookNotFoundMsg
# No such book found: (code #100)
print(BookNotFoundMsg)
# No such route: The API doesn't know endpoint of such URI. (code #101)
print(RouteNotFoundMsg)
Or accessing individual fields:
# No such route
print(RouteNotFoundMsg.title)
Exceptions#
Also, passing to the exception plays nicely:
# ValueError: No such book found: (code #100)
raise ValueError(BookNotFoundMsg)
# ValueError: No such route: The API doesn't know endpoint of such URI. (code #101)
raise ValueError(RouteNotFoundMsg)
Falcon Web framework#
Our backend technology stack uses amazing Falcon, a minimalist WSGI/ASGI framework for REST APIs.
In Falcon, raising falcon.HTTPError
(and children) gives API users beautiful error messages out of the box. These exceptions accept title
, description
, and code
kwargs.
For example,
raise falcon.HTTPNotFound(title="Book not found")
refactored to
raise falcon.HTTPNotFound(**BookNotFoundMsg.to_dict())
produces
HTTP/1.1 404 Not Found
Server: gunicorn/20.0.4
Date: Fri, 14 Jul 2023 12:01:19 GMT
Connection: close
content-type: application/json
vary: Accept
content-length: 37
{"title: "Share not found", "description": "Share with specified digest has not been found.", "code": 102}
Falcon even support adding a link to help page with link
kwarg.
Comments
comments powered by Disqus