hack-house/.venv/lib/python3.12/site-packages/sanic_routing/group.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

207 lines
6.2 KiB
Python

from __future__ import annotations
from typing import FrozenSet, List, Optional, Sequence, Tuple
from sanic_routing.route import Requirements, Route
from sanic_routing.utils import Immutable
from .exceptions import InvalidUsage, RouteExists
class RouteGroup:
methods_index: Immutable
passthru_properties = (
"labels",
"params",
"parts",
"path",
"pattern",
"raw_path",
"regex",
"router",
"segments",
"strict",
"unquote",
"uri",
)
#: The _reconstructed_ path after the Route has been normalized.
#: Does not contain preceding ``/`` (see also
#: :py:attr:`uri`)
path: str
#: A regex version of the :py:attr:`~sanic_routing.route.Route.path`
pattern: Optional[str]
#: Whether the route requires regular expression evaluation
regex: bool
#: The raw version of the path exploded (see also
#: :py:attr:`segments`)
parts: Tuple[str, ...]
#: Same as :py:attr:`parts` except
#: generalized so that any dynamic parts do not
#: include param keys since they have no impact on routing.
segments: Tuple[str, ...]
#: Whether the route should be matched with strict evaluation
strict: bool
#: Whether the route should be unquoted after matching if (for example) it
#: is suspected to contain non-URL friendly characters
unquote: bool
#: Since :py:attr:`path` does NOT
#: include a preceding '/', this adds it back.
uri: str
def __init__(self, *routes) -> None:
if len(set(route.parts for route in routes)) > 1:
raise InvalidUsage("Cannot group routes with differing paths")
if any(routes[-1].strict != route.strict for route in routes):
raise InvalidUsage("Cannot group routes with differing strictness")
route_list = list(routes)
route_list.pop()
self._routes = routes
self.pattern_idx = 0
def __str__(self):
display = (
f"path={self.path or self.router.delimiter} len={len(self.routes)}"
)
return f"<{self.__class__.__name__}: {display}>"
def __repr__(self) -> str:
return str(self)
def __iter__(self):
return iter(self.routes)
def __getitem__(self, key):
return self.routes[key]
def __getattr__(self, key):
# There are a number of properties that all of the routes in the group
# share in common. We pass thrm through to make them available
# on the RouteGroup, and then cache them so that they are permanent.
if key in self.passthru_properties:
value = getattr(self[0], key)
setattr(self, key, value)
return value
raise AttributeError(f"RouteGroup has no '{key}' attribute")
def finalize(self):
self.methods_index = Immutable(
{
method: route
for route in self._routes
for method in route.methods
}
)
def prioritize_routes(self) -> None:
"""
Sorts the routes in the group by priority
"""
self._routes = tuple(
sorted(self._routes, key=lambda route: route.priority)
)
def reset(self):
self.methods_index = dict(self.methods_index)
def merge(
self, group: RouteGroup, overwrite: bool = False, append: bool = False
) -> None:
"""
The purpose of merge is to group routes with the same path, but
declarared individually. In other words to group these:
.. code-block:: python
@app.get("/path/to")
def handler1(...):
...
@app.post("/path/to")
def handler2(...):
...
The other main purpose is to look for conflicts and
raise ``RouteExists``
A duplicate route is when:
1. They have the same path and any overlapping methods; AND
2. If they have requirements, they are the same
:param group: Incoming route group
:type group: RouteGroup
:param overwrite: whether to allow an otherwise duplicate route group
to overwrite the existing, if ``True`` will not raise exception
on duplicates, defaults to False
:type overwrite: bool, optional
:param append: whether to allow an otherwise duplicate route group to
append its routes to the existing route group, defaults to False
:type append: bool, optional
:raises RouteExists: Raised when there is a duplicate
"""
_routes = list(self._routes)
for other_route in group.routes:
for current_route in self:
if (
current_route == other_route
or (
current_route.requirements
and not other_route.requirements
)
or (
not current_route.requirements
and other_route.requirements
)
) and not append:
if not overwrite:
raise RouteExists(
f"Route already registered: {self.raw_path} "
f"[{','.join(self.methods)}]"
)
else:
_routes.append(other_route)
_routes.sort(
key=lambda route: route.priority, reverse=True
)
self._routes = tuple(_routes)
@property
def depth(self) -> int:
"""
The number of parts in :py:attr:`parts`
"""
return len(self[0].parts)
@property
def dynamic_path(self) -> bool:
return any(
(param.label == "path") or ("/" in param.label)
for param in self.params.values()
)
@property
def methods(self) -> FrozenSet[str]:
""""""
return frozenset(
[method for route in self for method in route.methods]
)
@property
def routes(self) -> Sequence[Route]:
return self._routes
@property
def requirements(self) -> List[Requirements]:
return [route.requirements for route in self if route.requirements]