Some checks failed
Lint Code / lint (push) Failing after 44s
CI/CD Pipeline / lint (push) Successful in 0s
Run Tests / test (3.12) (push) Successful in 3m48s
Run Tests / test (3.13) (push) Successful in 3m7s
CI/CD Pipeline / test (push) Successful in 1s
CI/CD Pipeline / build-and-release (push) Has been skipped
CI/CD Pipeline / notify (push) Successful in 1s
130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""
|
|
Pure Python fallback for _routing when PCRE2/Cython is not available.
|
|
|
|
This module provides the same interface using the standard library `re` module.
|
|
It's slower than the Cython+PCRE2 implementation but works everywhere.
|
|
In future we may add pcre2.py library support for better performance in this module.
|
|
"""
|
|
|
|
import re
|
|
from typing import Any, Dict, List, Optional, Pattern, Tuple
|
|
|
|
|
|
class FastRouteMatch:
|
|
__slots__ = ("config", "params")
|
|
|
|
def __init__(self, config: Dict[str, Any], params: Optional[Dict[str, str]] = None):
|
|
self.config = config
|
|
self.params = params if params is not None else {}
|
|
|
|
|
|
class FastRouter:
|
|
"""
|
|
Router with regex pattern matching.
|
|
|
|
Matching order (nginx-like):
|
|
1. Exact routes (prefix "=") - O(1) dict lookup
|
|
2. Regex routes (prefix "~" or "~*") - linear scan
|
|
3. Default route (fallback)
|
|
"""
|
|
|
|
__slots__ = ("_exact_routes", "_regex_routes", "_default_route", "_has_default", "_regex_count")
|
|
|
|
def __init__(self) -> None:
|
|
self._exact_routes: Dict[str, Dict[str, Any]] = {}
|
|
self._regex_routes: List[Tuple[Pattern[str], Dict[str, Any]]] = []
|
|
self._default_route: Dict[str, Any] = {}
|
|
self._has_default: bool = False
|
|
self._regex_count: int = 0
|
|
|
|
def add_route(self, pattern: str, config: Dict[str, Any]) -> None:
|
|
if pattern.startswith("="):
|
|
exact_path = pattern[1:]
|
|
self._exact_routes[exact_path] = config
|
|
return
|
|
|
|
if pattern == "__default__":
|
|
self._default_route = config
|
|
self._has_default = True
|
|
return
|
|
|
|
if pattern.startswith("~"):
|
|
case_insensitive = pattern.startswith("~*")
|
|
regex_pattern = pattern[2:] if case_insensitive else pattern[1:]
|
|
|
|
flags = re.IGNORECASE if case_insensitive else 0
|
|
try:
|
|
compiled_pattern = re.compile(regex_pattern, flags)
|
|
self._regex_routes.append((compiled_pattern, config))
|
|
self._regex_count = len(self._regex_routes)
|
|
except re.error:
|
|
pass # Ignore invalid patterns
|
|
|
|
def match(self, path: str) -> Optional[FastRouteMatch]:
|
|
if path in self._exact_routes:
|
|
config = self._exact_routes[path]
|
|
return FastRouteMatch(config, {})
|
|
|
|
for pattern, config in self._regex_routes:
|
|
match_obj = pattern.search(path)
|
|
if match_obj is not None:
|
|
params = match_obj.groupdict()
|
|
return FastRouteMatch(config, params)
|
|
|
|
if self._has_default:
|
|
return FastRouteMatch(self._default_route, {})
|
|
|
|
return None
|
|
|
|
@property
|
|
def exact_routes(self) -> Dict[str, Dict[str, Any]]:
|
|
return self._exact_routes
|
|
|
|
@property
|
|
def routes(self) -> Dict[Pattern[str], Dict[str, Any]]:
|
|
return {p: c for p, c in self._regex_routes}
|
|
|
|
@property
|
|
def default_route(self) -> Optional[Dict[str, Any]]:
|
|
return self._default_route if self._has_default else None
|
|
|
|
def list_routes(self) -> List[Dict[str, Any]]:
|
|
result: List[Dict[str, Any]] = []
|
|
|
|
for path, config in self._exact_routes.items():
|
|
result.append({
|
|
"type": "exact",
|
|
"pattern": f"={path}",
|
|
"config": config,
|
|
})
|
|
|
|
for pattern, config in self._regex_routes:
|
|
result.append({
|
|
"type": "regex",
|
|
"pattern": pattern.pattern,
|
|
"config": config,
|
|
})
|
|
|
|
if self._has_default:
|
|
result.append({
|
|
"type": "default",
|
|
"pattern": "__default__",
|
|
"config": self._default_route,
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
def fast_match(router: FastRouter, path: str) -> Optional[FastRouteMatch]:
|
|
"""
|
|
Convenience function for matching a path.
|
|
|
|
Args:
|
|
router: FastRouter instance
|
|
path: URL path to match
|
|
|
|
Returns:
|
|
FastRouteMatch or None
|
|
"""
|
|
return router.match(path)
|