hack-house/.venv/lib/python3.12/site-packages/sanic_routing/patterns.py
leetcrypt bb1d662ee1 chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:29:14 -07:00

179 lines
5.7 KiB
Python

import re
import typing as t
import uuid
from datetime import date, datetime
from types import SimpleNamespace
from typing import Any, Callable, Dict, Pattern, Tuple, Type
from sanic_routing.exceptions import InvalidUsage, NotFound
def parse_date(d) -> date:
return datetime.strptime(d, "%Y-%m-%d").date()
def alpha(param: str) -> str:
if not param.isalpha():
raise ValueError(f"Value {param} contains non-alphabetic chracters")
return param
def slug(param: str) -> str:
if not REGEX_TYPES["slug"][1].match(param):
raise ValueError(f"Value {param} does not match the slug format")
return param
def ext(param: str) -> Tuple[str, ...]:
parts = tuple(param.split("."))
if any(not p for p in parts) or len(parts) == 1:
raise ValueError(f"Value {param} does not match filename format")
return parts
def nonemptystr(param: str) -> str:
if not param:
raise ValueError(f"Value {param} is an empty string")
return param
class ParamInfo:
__slots__ = (
"cast",
"ctx",
"label",
"name",
"pattern",
"priority",
"raw_path",
"regex",
)
def __init__(
self,
name: str,
raw_path: str,
label: str,
cast: t.Callable[[str], t.Any],
pattern: re.Pattern,
regex: bool,
priority: int,
) -> None:
self.name = name
self.raw_path = raw_path
self.label = label
self.cast = cast
self.pattern = pattern
self.regex = regex
self.priority = priority
self.ctx = SimpleNamespace()
def process(
self,
params: t.Dict[str, t.Any],
value: t.Union[str, t.Tuple[str, ...]],
) -> None:
params[self.name] = value
class ExtParamInfo(ParamInfo):
def __init__(self, **kwargs):
super().__init__(**kwargs)
match = REGEX_PARAM_EXT_PATH.search(self.raw_path)
if not match:
raise InvalidUsage(
f"Invalid extension parameter definition: {self.raw_path}"
)
if match.group(2) == "path":
raise InvalidUsage(
"Extension parameter matching does not support the "
"`path` type."
)
ext_type = match.group(3)
regex_type = REGEX_TYPES.get(match.group(2))
self.ctx.cast = None
if regex_type:
self.ctx.cast = regex_type[0]
elif match.group(2):
raise InvalidUsage(
"Extension parameter matching only supports filename matching "
"on known parameter types, and not regular expressions."
)
self.ctx.allowed = []
self.ctx.allowed_sub_count = 0
if ext_type:
self.ctx.allowed = ext_type.split("|")
allowed_subs = {allowed.count(".") for allowed in self.ctx.allowed}
if len(allowed_subs) > 1:
raise InvalidUsage(
"All allowed extensions within a single route definition "
"must contain the same number of subparts. For example: "
"<foo:ext=js|css> and <foo:ext=min.js|min.css> are both "
"acceptable, but <foo:ext=js|min.js> is not."
)
self.ctx.allowed_sub_count = next(iter(allowed_subs))
for extension in self.ctx.allowed:
if not REGEX_ALLOWED_EXTENSION.match(extension):
raise InvalidUsage(f"Invalid extension: {extension}")
def process(self, params, value):
stop = -1 * (self.ctx.allowed_sub_count + 1)
filename = ".".join(value[:stop])
ext = ".".join(value[stop:])
if self.ctx.allowed and ext not in self.ctx.allowed:
raise NotFound(f"Invalid extension: {ext}")
if self.ctx.cast:
try:
filename = self.ctx.cast(filename)
except ValueError:
raise NotFound(f"Invalid filename: {filename}")
params[self.name] = filename
params["ext"] = ext
EXTENSION = r"[a-z0-9](?:[a-z0-9\.]*[a-z0-9])?"
PARAM_EXT = (
r"<([a-zA-Z_][a-zA-Z0-9_]*)(?:=([a-z]+))?(?::ext(?:=([a-z0-9|\.]+))?)>"
)
REGEX_PARAM_NAME = re.compile(r"^<([a-zA-Z_][a-zA-Z0-9_]*)(?::(.*))?>$")
REGEX_PARAM_EXT_PATH = re.compile(PARAM_EXT)
REGEX_PARAM_NAME_EXT = re.compile(r"^" + PARAM_EXT + r"$")
REGEX_ALLOWED_EXTENSION = re.compile(r"^" + EXTENSION + r"$")
# Predefined path parameter types. The value is a tuple consisteing of a
# callable and a compiled regular expression.
# The callable should:
# 1. accept a string input
# 2. cast the string to desired type
# 3. raise ValueError if it cannot
# The regular expression is generally NOT used. Unless the path is forced
# to use regex patterns.
REGEX_TYPES_ANNOTATION = Dict[
str, Tuple[Callable[[str], Any], Pattern, Type[ParamInfo]]
]
REGEX_TYPES: REGEX_TYPES_ANNOTATION = {
"strorempty": (str, re.compile(r"^[^/]*$"), ParamInfo),
"str": (nonemptystr, re.compile(r"^[^/]+$"), ParamInfo),
"ext": (ext, re.compile(r"^[^/]+\." + EXTENSION + r"$"), ExtParamInfo),
"slug": (slug, re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$"), ParamInfo),
"alpha": (alpha, re.compile(r"^[A-Za-z]+$"), ParamInfo),
"path": (str, re.compile(r"^[^/]?.*?$"), ParamInfo),
"float": (float, re.compile(r"^-?(?:\d+(?:\.\d*)?|\.\d+)$"), ParamInfo),
"int": (int, re.compile(r"^-?\d+$"), ParamInfo),
"ymd": (
parse_date,
re.compile(r"^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))$"),
ParamInfo,
),
"uuid": (
uuid.UUID,
re.compile(
r"^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-"
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$"
),
ParamInfo,
),
}