Allowing Enums or Literals in Path parameters
🚀 Feature Request
Hi. Is it possible to use Literals or Enums with route parameters? This would help users in the auto OpenAPI documentation generation to see valid parameters.
Consider the following real-world scenario:
# imports omitted for brevity
class Documents(CortexAPIController):
@classmethod
def version(cls) -> str:
return "v1"
@classmethod
def route(cls):
return "/documentos-institucionais/"
@docs(extract_document_contents_docs)
@post("/{document_type}/extracoes")
async def extract_contents_async(
self,
files: FromFiles,
document_type: str,
extraction_service: InsitutionalDocumentInfoExtractionProtocol,
consumer_id: FromConsumerIDHeader,
) -> StructuredDocumentType:
data = files.value
form_part = data[0]
if form_part.file_name is None:
raise BadRequest("The provided file does not contain any name")
file_contents = form_part.data
path = Path(form_part.file_name.decode("utf-8"))
extension = path.suffix.lstrip(".")
try:
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, f"temp.{extension}")
with open(temp_file_path, "wb") as temp_file:
temp_file.write(file_contents)
return await extraction_service.extract_info_async(
file_path=temp_file_path,
declared_type=DocumentType(document_type), # this woudn' be necessary anymore!
consumer_id=consumer_id.value,
)
except InvalidDocumentException as e:
raise BadRequest(str(e))
It would be really nice to have "document_type" in this case, typed with a Literal or Enum. But in both cases I have some errors. I tried to research this in the docs, but I found nothing related to this. Is this possible with current BlackSheep version?
Hi Arthur, Good point! The built-in classes to bind route values do not support literals or enums. Support for these could be added, it would be a good idea.
BlackSheep offers an API to define custom binders. I prepared an example for you:
from enum import StrEnum
from urllib.request import Request
from openapidocs.v3 import Info
from blacksheep import Application
from blacksheep.exceptions import BadRequest
from blacksheep.server.bindings import Binder, BoundValue, T
from blacksheep.server.openapi.v3 import OpenAPIHandler
class DocumentType(StrEnum):
ONE = "One"
TWO = "Two"
THREE = "Three"
class EnumFromRoute(BoundValue[T]):
pass
class EnumFromRouteBinder(Binder):
handle = EnumFromRoute
async def get_value(self, request: Request):
value = request.route_values.get(self.parameter_name)
try:
return self.expected_type(value)
except ValueError:
raise BadRequest(
f"Invalid parameter: '{value}' "
f"is not a valid {self.expected_type.__name__}"
)
app = Application()
docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)
@app.router.get("/{document_type}")
def home(document_type: EnumFromRoute[DocumentType]):
return f"Hello World {document_type}"
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, port=44777)
curl http://localhost:44777/Two
Hello World <EnumFromRoute(Two)>
curl http://localhost:44777/two
Bad Request: Invalid parameter: 'two' is not a valid DocumentType
Note that in this case, since the Enum is obtained with self.expected_type(value), values are case sensitive.
Additional code is needed for a case insensitive check.
For literals, you would need introspecting code to validate the possible values:
from typing import get_args, Literal
def validate_literal(literal_type, value: str):
allowed = get_args(literal_type)
for allowed_value in allowed:
if isinstance(allowed_value, str) and allowed_value.lower() == value.lower():
return allowed_value
if allowed_value == value:
return allowed_value
raise ValueError(f"{value!r} is not a valid {literal_type}")
Hi, Roberto. Thanks for the feedback! I'll consider this implementation for now. Thanks a lot!
Thanks, Arthur! I reopened the issue because I would like to add support for StrEnum and IntEnum (I started this). 👍🏼
Thanks, Arthur! I reopened the issue because I would like to add support for StrEnum and IntEnum (I started this). 👍🏼
Oh. Okay! Thanks! I think this would be a nice addition to BlackSheep.
Done at https://github.com/Neoteroi/BlackSheep/pull/597.