eptm_dashboard/.venv/lib/python3.12/site-packages/reflex/route.py

226 lines
7.7 KiB
Python

"""The route decorator and associated variables and functions."""
from __future__ import annotations
import re
from collections.abc import Callable
from reflex_base import constants
from reflex_base.config import get_config
def verify_route_validity(route: str) -> None:
"""Verify if the route is valid, and throw an error if not.
Args:
route: The route that need to be checked
Raises:
ValueError: If the route is invalid.
"""
route_parts = route.removeprefix("/").split("/")
for i, part in enumerate(route_parts):
if constants.RouteRegex.SLUG.fullmatch(part):
continue
if not part.startswith("[") or not part.endswith("]"):
msg = (
f"Route part `{part}` is not valid. Reflex only supports "
"alphabetic characters, underscores, and hyphens in route parts. "
)
raise ValueError(msg)
if part.startswith(("[[...", "[...")):
if part != constants.RouteRegex.SPLAT_CATCHALL:
msg = f"Catchall pattern `{part}` is not valid. Only `{constants.RouteRegex.SPLAT_CATCHALL}` is allowed."
raise ValueError(msg)
if i != len(route_parts) - 1:
msg = f"Catchall pattern `{part}` must be at the end of the route."
raise ValueError(msg)
continue
if part.startswith("[["):
if constants.RouteRegex.OPTIONAL_ARG.fullmatch(part):
continue
msg = (
f"Route part `{part}` with optional argument is not valid. "
"Reflex only supports optional arguments that start with an alphabetic character or underscore, "
"followed by alphanumeric characters or underscores."
)
raise ValueError(msg)
if not constants.RouteRegex.ARG.fullmatch(part):
msg = (
f"Route part `{part}` with argument is not valid. "
"Reflex only supports argument names that start with an alphabetic character or underscore, "
"followed by alphanumeric characters or underscores."
)
raise ValueError(msg)
def get_route_args(route: str) -> dict[str, str]:
"""Get the dynamic arguments for the given route.
Args:
route: The route to get the arguments for.
Returns:
The route arguments.
"""
args = {}
def _add_route_arg(arg_name: str, type_: str):
if arg_name in args:
msg = (
f"Arg name `{arg_name}` is used more than once in the route `{route}`."
)
raise ValueError(msg)
args[arg_name] = type_
# Regex to check for route args.
argument_regex = constants.RouteRegex.ARG
optional_argument_regex = constants.RouteRegex.OPTIONAL_ARG
# Iterate over the route parts and check for route args.
for part in route.split("/"):
if part == constants.RouteRegex.SPLAT_CATCHALL:
_add_route_arg("splat", constants.RouteArgType.LIST)
break
optional_argument = optional_argument_regex.match(part)
if optional_argument:
_add_route_arg(optional_argument.group(1), constants.RouteArgType.SINGLE)
continue
argument = argument_regex.match(part)
if argument:
_add_route_arg(argument.group(1), constants.RouteArgType.SINGLE)
continue
return args
def replace_brackets_with_keywords(input_string: str) -> str:
"""Replace brackets and everything inside it in a string with a keyword.
Example:
>>> replace_brackets_with_keywords("/posts")
'/posts'
>>> replace_brackets_with_keywords("/posts/[slug]")
'/posts/__SINGLE_SEGMENT__'
>>> replace_brackets_with_keywords("/posts/[slug]/comments")
'/posts/__SINGLE_SEGMENT__/comments'
>>> replace_brackets_with_keywords("/posts/[[slug]]")
'/posts/__DOUBLE_SEGMENT__'
>>> replace_brackets_with_keywords("/posts/[[...splat]]")
'/posts/__DOUBLE_CATCHALL_SEGMENT__'
Args:
input_string: String to replace.
Returns:
new string containing keywords.
"""
# Replace [<slug>] with __SINGLE_SEGMENT__
return constants.RouteRegex.ARG.sub(
constants.RouteRegex.SINGLE_SEGMENT,
# Replace [[slug]] with __DOUBLE_SEGMENT__
constants.RouteRegex.OPTIONAL_ARG.sub(
constants.RouteRegex.DOUBLE_SEGMENT,
# Replace [[...splat]] with __DOUBLE_CATCHALL_SEGMENT__
input_string.replace(
constants.RouteRegex.SPLAT_CATCHALL,
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
),
),
)
def route_specificity(keyworded_route: str) -> tuple[int, int, int]:
"""Get the specificity of a route with keywords.
The smaller the number, the more specific the route is.
Args:
keyworded_route: The route with keywords.
Returns:
A tuple containing the counts of double catchall segments,
double segments, and single segments in the route.
"""
return (
keyworded_route.count(constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT),
keyworded_route.count(constants.RouteRegex.DOUBLE_SEGMENT),
keyworded_route.count(constants.RouteRegex.SINGLE_SEGMENT),
)
def get_route_regex(keyworded_route: str) -> re.Pattern:
"""Get the regex pattern for a route with keywords.
Args:
keyworded_route: The route with keywords.
Returns:
A compiled regex pattern for the route.
"""
if keyworded_route == "index":
return re.compile(re.escape("/"))
path_parts = keyworded_route.split("/")
regex_parts = []
for part in path_parts:
if part == constants.RouteRegex.SINGLE_SEGMENT:
# Match a single segment (/slug)
regex_parts.append(r"/[^/]*")
elif part == constants.RouteRegex.DOUBLE_SEGMENT:
# Match a single optional segment (/slug or nothing)
regex_parts.append(r"(/[^/]+)?")
elif part == constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT:
regex_parts.append(".*")
else:
regex_parts.append(re.escape("/" + part))
# Join the regex parts and compile the regex
regex_pattern = "".join(regex_parts)
regex_pattern = f"^{regex_pattern}/?$"
return re.compile(regex_pattern)
def get_router(routes: list[str]) -> Callable[[str], str | None]:
"""Get a function that computes the route for a given path.
Args:
routes: A list of routes to match against.
Returns:
A function that takes a path and returns the first matching route,
or None if no match is found.
"""
keyworded_routes = {
replace_brackets_with_keywords(route): route for route in routes
}
sorted_routes_by_specificity = sorted(
keyworded_routes.items(),
key=lambda item: route_specificity(item[0]),
)
regexed_routes = [
(get_route_regex(keyworded_route), original_route)
for keyworded_route, original_route in sorted_routes_by_specificity
]
def get_route(path: str) -> str | None:
"""Get the first matching route for a given path.
Args:
path: The path to match against the routes.
Returns:
The first matching route, or None if no match is found.
"""
config = get_config()
if config.frontend_path:
path = path.removeprefix(config.frontend_path)
path = "/" + path.removeprefix("/").removesuffix("/")
if path == "/index":
path = "/"
for regex, original_route in regexed_routes:
if regex.fullmatch(path):
return original_route
return None
return get_route